mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 17:17:37 +02:00
Settings improvements (#9412)
This commit is contained in:
@@ -1680,7 +1680,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
body.mobile .options-section table {
|
||||
body.mobile .options-section-card table {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -1860,12 +1860,12 @@ button.close:hover {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.options-section h5 {
|
||||
.options-section-card h5 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.options-section input[type="number"] {
|
||||
.options-section-card input[type="number"] {
|
||||
/* overriding settings from .form-control */
|
||||
width: 10em !important;
|
||||
flex-grow: 0 !important;
|
||||
|
||||
@@ -186,12 +186,7 @@ body.experimental-feature-new-layout .note-detail-content-widget-content.options
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) {
|
||||
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
|
||||
box-shadow: var(--card-box-shadow);
|
||||
border: 1px solid var(--card-border-color) !important;
|
||||
border-radius: 8px;
|
||||
background: var(--card-background-color);
|
||||
padding: var(--options-card-padding);
|
||||
margin-bottom: 26px !important;
|
||||
}
|
||||
|
||||
body.desktop .options-section:not(.tn-no-card) {
|
||||
@@ -199,40 +194,70 @@ body.desktop .options-section:not(.tn-no-card) {
|
||||
max-width: var(--options-card-max-width);
|
||||
}
|
||||
|
||||
.options-section-card {
|
||||
box-shadow: var(--card-box-shadow);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--card-background-color);
|
||||
padding: var(--options-card-padding);
|
||||
}
|
||||
|
||||
.note-detail-content-widget-content.options {
|
||||
--default-padding: 15px;
|
||||
padding-top: calc(var(--default-padding) + var(--options-title-offset) + var(--options-title-font-size));
|
||||
padding-top: var(--default-padding);
|
||||
padding-bottom: var(--default-padding);
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) h4,
|
||||
.options-section:not(.tn-no-card) h5 {
|
||||
.options-section:not(.tn-no-card) h4 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .4pt;
|
||||
}
|
||||
|
||||
|
||||
.options-section:not(.tn-no-card) h4 {
|
||||
font-size: var(--options-title-font-size);
|
||||
font-weight: 600;
|
||||
color: var(--launcher-pane-text-color);
|
||||
margin-top: calc(-1 * var(--options-card-padding) - var(--options-title-font-size) - var(--options-title-offset)) !important;
|
||||
margin-bottom: calc(var(--options-title-offset) + var(--options-card-padding)) !important;
|
||||
margin-inline-start: calc(-1 * var(--options-card-padding));
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--options-title-offset);
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) h5 {
|
||||
.options-section:not(.tn-no-card) .options-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) .options-section-header h4 {
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) .options-section-header .icon-action {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.options-section-description {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 13px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.options-section-card h5 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .4pt;
|
||||
font-size: var(--options-title-font-size);
|
||||
font-weight: bold;
|
||||
margin-top: 1em !important;
|
||||
margin-bottom: unset !important;
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) h5:first-of-type {
|
||||
.options-section-card h5:first-of-type {
|
||||
margin-top: unset !important;
|
||||
}
|
||||
|
||||
.options-section hr {
|
||||
.options-section-card hr {
|
||||
--bs-border-width: 2px;
|
||||
|
||||
margin-inline-start: calc(var(--options-card-padding) * -1);
|
||||
@@ -241,27 +266,26 @@ body.desktop .options-section:not(.tn-no-card) {
|
||||
color: var(--root-background);
|
||||
}
|
||||
|
||||
.options-section p:last-of-type:not(:first-of-type),
|
||||
.options-section h4 + p:last-child,
|
||||
.options-section .existing-anonymized-databases {
|
||||
.options-section-card p:last-of-type:not(:first-of-type),
|
||||
.options-section-card .existing-anonymized-databases {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.options-section .form-group {
|
||||
.options-section-card .form-group {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.options-section .form-group:last-child {
|
||||
.options-section-card .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.options-section ul {
|
||||
.options-section-card ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.options-section label:not(.tn-checkbox):not(.tn-radio) {
|
||||
.options-section-card label:not(.tn-checkbox):not(.tn-radio) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -275,7 +299,7 @@ nav.options-section-tabs .nav-tabs {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
nav.options-section-tabs + .options-section {
|
||||
nav.options-section-tabs + .options-section .options-section-card {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
@@ -329,3 +353,9 @@ nav.options-section-tabs + .options-section {
|
||||
.etapi-options-section div {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* BACKUP */
|
||||
|
||||
.options-section-card table a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -1121,17 +1121,20 @@
|
||||
},
|
||||
"consistency_checks": {
|
||||
"title": "Consistency Checks",
|
||||
"find_and_fix_label": "Find and fix consistency issues",
|
||||
"find_and_fix_description": "Scan for and automatically repair any data consistency issues in the database.",
|
||||
"find_and_fix_button": "Find and fix consistency issues",
|
||||
"finding_and_fixing_message": "Finding and fixing consistency issues...",
|
||||
"issues_fixed_message": "Any consistency issue which may have been found is now fixed."
|
||||
},
|
||||
"database_anonymization": {
|
||||
"title": "Database Anonymization",
|
||||
"full_anonymization": "Full Anonymization",
|
||||
"full_anonymization_description": "This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata) for sharing online for debugging purposes without fear of leaking your personal data.",
|
||||
"description": "Create an anonymized copy of your database for sharing with developers when debugging issues, without exposing personal data.",
|
||||
"full_anonymization": "Full anonymization",
|
||||
"full_anonymization_description": "Creates a copy of the database with all note content removed, leaving only structure and non-sensitive metadata. Safe for sharing online when debugging issues.",
|
||||
"save_fully_anonymized_database": "Save fully anonymized database",
|
||||
"light_anonymization": "Light Anonymization",
|
||||
"light_anonymization_description": "This action will create a new copy of the database and do a light anonymization on it — specifically only content of all notes will be removed, but titles and attributes will remain. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.",
|
||||
"light_anonymization": "Light anonymization",
|
||||
"light_anonymization_description": "Creates a copy with note content removed, but titles, attributes, and custom scripts/widgets remain. Provides more context for debugging.",
|
||||
"choose_anonymization": "You can decide yourself if you want to provide a fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.",
|
||||
"save_lightly_anonymized_database": "Save lightly anonymized database",
|
||||
"existing_anonymized_databases": "Existing anonymized databases",
|
||||
@@ -1144,7 +1147,8 @@
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "Database Integrity Check",
|
||||
"description": "This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.",
|
||||
"check_integrity_label": "Check database integrity",
|
||||
"check_integrity_description": "Verify that the database is not corrupted on the SQLite level.",
|
||||
"check_button": "Check database integrity",
|
||||
"checking_integrity": "Checking database integrity...",
|
||||
"integrity_check_succeeded": "Integrity check succeeded - no problems found.",
|
||||
@@ -1152,7 +1156,11 @@
|
||||
},
|
||||
"sync": {
|
||||
"title": "Sync",
|
||||
"force_full_sync_label": "Force full sync",
|
||||
"force_full_sync_description": "Trigger a complete synchronization with the sync server, re-uploading all changes.",
|
||||
"force_full_sync_button": "Force full sync",
|
||||
"fill_entity_changes_label": "Fill entity changes",
|
||||
"fill_entity_changes_description": "Rebuild entity change records. Use this if sync is missing some changes.",
|
||||
"fill_entity_changes_button": "Fill entity changes records",
|
||||
"full_sync_triggered": "Full sync triggered",
|
||||
"filling_entity_changes": "Filling entity changes rows...",
|
||||
@@ -1160,8 +1168,13 @@
|
||||
"finished-successfully": "Sync finished successfully.",
|
||||
"failed": "Sync failed: {{message}}"
|
||||
},
|
||||
"database": {
|
||||
"title": "Database"
|
||||
},
|
||||
"vacuum_database": {
|
||||
"title": "Vacuum Database",
|
||||
"vacuum_label": "Vacuum database",
|
||||
"vacuum_description": "Rebuild the database to reduce file size. No data will be changed.",
|
||||
"description": "This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.",
|
||||
"button_text": "Vacuum database",
|
||||
"vacuuming_database": "Vacuuming database...",
|
||||
@@ -1178,16 +1191,18 @@
|
||||
"fonts": {
|
||||
"theme_defined": "Theme defined",
|
||||
"fonts": "Fonts",
|
||||
"main_font": "Main Font",
|
||||
"custom_fonts": "Use custom fonts",
|
||||
"main_font": "Interface text",
|
||||
"font_family": "Font family",
|
||||
"size": "Size",
|
||||
"note_tree_font": "Note Tree Font",
|
||||
"note_detail_font": "Note Detail Font",
|
||||
"monospace_font": "Monospace (code) Font",
|
||||
"note_tree_and_detail_font_sizing": "Note that tree and detail font sizing is relative to the main font size setting.",
|
||||
"not_all_fonts_available": "Not all listed fonts may be available on your system.",
|
||||
"apply_font_changes": "To apply font changes, click on",
|
||||
"reload_frontend": "reload frontend",
|
||||
"preview": "Preview",
|
||||
"note_tree_font": "Note tree text",
|
||||
"note_detail_font": "Document text",
|
||||
"monospace_font": "Monospace text",
|
||||
"monospace_font_description": "Used for code notes and code blocks",
|
||||
"size_relative_to_general": "Size is relative to the general font size",
|
||||
"not_all_fonts_available": "Not all listed fonts may be available on your system",
|
||||
"apply_changes": "Reload to apply changes",
|
||||
"generic-fonts": "Generic fonts",
|
||||
"sans-serif-system-fonts": "Sans-serif system fonts",
|
||||
"serif-system-fonts": "Serif system fonts",
|
||||
@@ -1216,15 +1231,18 @@
|
||||
"edited_notes_message": "Edited Notes ribbon tab will automatically open on day notes"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Application Theme",
|
||||
"theme_label": "Theme",
|
||||
"title": "User Interface",
|
||||
"theme_label": "Application theme",
|
||||
"override_theme_fonts_label": "Override theme fonts",
|
||||
"auto_theme": "Legacy (Follow system color scheme)",
|
||||
"light_theme": "Legacy (Light)",
|
||||
"dark_theme": "Legacy (Dark)",
|
||||
"triliumnext": "Trilium (Follow system color scheme)",
|
||||
"triliumnext-light": "Trilium (Light)",
|
||||
"triliumnext-dark": "Trilium (Dark)",
|
||||
"auto_theme": "Follow system color scheme",
|
||||
"light_theme": "Light",
|
||||
"dark_theme": "Dark",
|
||||
"triliumnext": "Follow system color scheme",
|
||||
"triliumnext-light": "Light",
|
||||
"triliumnext-dark": "Dark",
|
||||
"modern_themes": "Modern",
|
||||
"legacy_themes": "Legacy",
|
||||
"custom_themes": "Custom",
|
||||
"layout": "Layout",
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-horizontal-title": "Horizontal",
|
||||
@@ -1233,11 +1251,11 @@
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Performance",
|
||||
"enable-motion": "Enable transitions and animations",
|
||||
"enable-shadows": "Enable shadows",
|
||||
"enable-backdrop-effects": "Enable background effects for menus, popups and panels",
|
||||
"enable-smooth-scroll": "Enable smooth scrolling",
|
||||
"app-restart-required": "(a restart of the application is required for the change to take effect)"
|
||||
"enable-motion": "Transitions and animations",
|
||||
"enable-shadows": "Shadows",
|
||||
"enable-backdrop-effects": "Background effects for menus, popups and panels",
|
||||
"enable-smooth-scroll": "Smooth scrolling",
|
||||
"app-restart-required": "Requires app restart"
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Zoom Factor (desktop build only)",
|
||||
@@ -1246,7 +1264,7 @@
|
||||
"code_auto_read_only_size": {
|
||||
"title": "Automatic Read-Only Size",
|
||||
"description": "Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).",
|
||||
"label": "Automatic read-only size (code notes)",
|
||||
"label": "Automatic read-only size",
|
||||
"unit": "characters"
|
||||
},
|
||||
"code-editor-options": {
|
||||
@@ -1292,36 +1310,45 @@
|
||||
"batch_ocr_error": "Error during batch processing: {{error}}"
|
||||
},
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_erasure_timeout": "Attachment Erasure Timeout",
|
||||
"attachment_auto_deletion_description": "Attachments get automatically deleted (and erased) if they are not referenced by their note anymore after a defined time out.",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"manual_erasing_description": "You can also trigger erasing manually (without considering the timeout defined above):",
|
||||
"erase_unused_attachments_now": "Erase unused attachment notes now",
|
||||
"attachment_erasure_timeout": "Unused Attachments",
|
||||
"description": "Attachments that are no longer referenced by any note are considered unused and can be automatically erased after a period of time.",
|
||||
"erase_attachments_after": "Erase unused attachments after",
|
||||
"erase_attachments_after_description": "Time before unused attachments are permanently erased.",
|
||||
"manual_erasing_description": "Trigger erasing manually, ignoring the timeout above.",
|
||||
"erase_unused_attachments_now": "Erase unused attachments now",
|
||||
"unused_attachments_erased": "Unused attachments have been erased."
|
||||
},
|
||||
"network_connections": {
|
||||
"network_connections_title": "Network Connections",
|
||||
"check_for_updates": "Check for updates automatically"
|
||||
"network_connections_title": "Network",
|
||||
"check_for_updates": "Check for updates automatically",
|
||||
"check_for_updates_description": "Checks for new versions on GitHub and shows a notification in the global menu when available."
|
||||
},
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "Note Erasure Timeout",
|
||||
"note_erasure_description": "Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them from Recent Notes dialog. After a period of time, deleted notes are \"erased\" which means their content is not recoverable anymore. This setting allows you to configure the length of the period between deleting and erasing the note.",
|
||||
"erase_notes_after": "Erase notes after:",
|
||||
"manual_erasing_description": "You can also trigger erasing manually (without considering the timeout defined above):",
|
||||
"note_erasure_timeout_title": "Deleted Notes",
|
||||
"description": "Deleted notes are first only marked as deleted and can be recovered from Recent Notes. After a period of time, they are permanently erased.",
|
||||
"erase_notes_after": "Erase notes after",
|
||||
"erase_notes_after_description": "Time before deleted notes are permanently erased.",
|
||||
"manual_erasing_description": "Trigger erasing manually, ignoring the timeout above.",
|
||||
"erase_deleted_notes_now": "Erase deleted notes now",
|
||||
"deleted_notes_erased": "Deleted notes have been erased."
|
||||
},
|
||||
"revisions": {
|
||||
"title": "Note Revisions"
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <doc>wiki</doc> for more info.",
|
||||
"snapshot_time_interval_label": "Note revision snapshot time interval:"
|
||||
"note_revisions_snapshot_description_short": "Time after which a new note revision will be created.",
|
||||
"snapshot_time_interval_label": "Snapshot interval"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "Note Revision Snapshot Limit",
|
||||
"note_revisions_snapshot_limit_description": "The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions. You can set the maximum revisions for a single note through the #versioningLimit label.",
|
||||
"snapshot_number_limit_label": "Note revision snapshot number limit:",
|
||||
"note_revisions_snapshot_limit_description_short": "Max revisions per note. Use -1 for unlimited, 0 to disable.",
|
||||
"snapshot_number_limit_label": "Maximum revisions",
|
||||
"snapshot_number_limit_unit": "snapshots",
|
||||
"erase_excess_revision_snapshots": "Erase excess revision snapshots now",
|
||||
"erase_excess_revision_snapshots_description": "Delete revisions exceeding the limit for all notes.",
|
||||
"erase_excess_revision_snapshots_prompt": "Excess revision snapshots have been erased."
|
||||
},
|
||||
"search": {
|
||||
@@ -1333,24 +1360,30 @@
|
||||
},
|
||||
"search_engine": {
|
||||
"title": "Search Engine",
|
||||
"custom_search_engine_info": "Custom search engine requires both a name and a URL to be set. If either of these is not set, DuckDuckGo will be used as the default search engine.",
|
||||
"predefined_templates_label": "Predefined search engine templates",
|
||||
"custom_search_engine_info": "Used when searching the web for selected text. If not configured, DuckDuckGo will be used.",
|
||||
"predefined_templates_label": "Presets",
|
||||
"bing": "Bing",
|
||||
"baidu": "Baidu",
|
||||
"duckduckgo": "DuckDuckGo",
|
||||
"google": "Google",
|
||||
"custom_name_label": "Custom search engine name",
|
||||
"custom_name_placeholder": "Customize search engine name",
|
||||
"custom_url_label": "Custom search engine URL should include {keyword} as a placeholder for the search term.",
|
||||
"custom_url_placeholder": "Customize search engine URL",
|
||||
"custom_name_label": "Name",
|
||||
"custom_name_placeholder": "Search engine name",
|
||||
"custom_url_label": "URL",
|
||||
"custom_url_description": "Use {keyword} as a placeholder for the search term.",
|
||||
"custom_url_placeholder": "Search engine URL",
|
||||
"save_button": "Save"
|
||||
},
|
||||
"tray": {
|
||||
"title": "System Tray",
|
||||
"enable_tray": "Enable tray (Trilium needs to be restarted for this change to take effect)"
|
||||
"enable_tray": "Tray icon",
|
||||
"enable_tray_description": "Trilium needs to be restarted for this change to take effect."
|
||||
},
|
||||
"text_editor": {
|
||||
"title": "Editor"
|
||||
},
|
||||
"heading_style": {
|
||||
"title": "Heading Style",
|
||||
"title": "Heading style",
|
||||
"description": "Visual style for headings in text notes.",
|
||||
"plain": "Plain",
|
||||
"underline": "Underline",
|
||||
"markdown": "Markdown-style"
|
||||
@@ -1377,14 +1410,16 @@
|
||||
"text_auto_read_only_size": {
|
||||
"title": "Automatic Read-Only Size",
|
||||
"description": "Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).",
|
||||
"label": "Automatic read-only size (text notes)",
|
||||
"label": "Automatic read-only size",
|
||||
"unit": "characters"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Custom Date/Time Format",
|
||||
"title": "Date/time format",
|
||||
"description": "Customize the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens.",
|
||||
"format_string": "Format string:",
|
||||
"formatted_time": "Formatted date/time:"
|
||||
"description_short": "Customize the format of the date and time inserted via the toolbar.",
|
||||
"preview": "Preview: {{preview}}",
|
||||
"format_string": "Format string",
|
||||
"formatted_time": "Formatted date/time"
|
||||
},
|
||||
"i18n": {
|
||||
"title": "Localization",
|
||||
@@ -1399,20 +1434,20 @@
|
||||
"sunday": "Sunday",
|
||||
"first-week-of-the-year": "First week of the year",
|
||||
"first-week-contains-first-day": "First week contains first day of the year",
|
||||
"first-week-contains-first-thursday": "First week contains first Thursday of the year",
|
||||
"first-week-contains-first-thursday": "First week contains first Thursday (ISO 8601)",
|
||||
"first-week-has-minimum-days": "First week has minimum days",
|
||||
"min-days-in-first-week": "Minimum days in first week",
|
||||
"first-week-info": "First week contains first Thursday of the year is based on <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a> standard.",
|
||||
"first-week-warning": "Changing first week options may cause duplicate with existing Week Notes and the existing Week Notes will not be updated accordingly.",
|
||||
"first-week-warning": "Changing this may cause duplicates with existing Week Notes.",
|
||||
"formatting-locale": "Date & number format",
|
||||
"formatting-locale-auto": "Based on the application's language"
|
||||
},
|
||||
"backup": {
|
||||
"title": "Backup",
|
||||
"automatic_backup": "Automatic backup",
|
||||
"automatic_backup_description": "Trilium can back up the database automatically:",
|
||||
"enable_daily_backup": "Enable daily backup",
|
||||
"enable_weekly_backup": "Enable weekly backup",
|
||||
"enable_monthly_backup": "Enable monthly backup",
|
||||
"enable_daily_backup": "Backup daily",
|
||||
"enable_weekly_backup": "Backup weekly",
|
||||
"enable_monthly_backup": "Backup monthly",
|
||||
"backup_recommendation": "It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.",
|
||||
"backup_now": "Backup now",
|
||||
"backup_database_now": "Backup database now",
|
||||
@@ -1456,11 +1491,15 @@
|
||||
"new_password": "New password",
|
||||
"new_password_confirmation": "New password confirmation",
|
||||
"change_password": "Change password",
|
||||
"protected_session_timeout": "Protected Session Timeout",
|
||||
"protected_session_timeout_description": "Protected session timeout is a time period after which the protected session is wiped from the browser's memory. This is measured from the last interaction with protected notes. See",
|
||||
"change_password_description": "Update your current password",
|
||||
"reset_password": "Reset password",
|
||||
"reset_password_description": "Permanently lose access to protected notes",
|
||||
"cancel": "Cancel",
|
||||
"protected_session_timeout": "Protected Session",
|
||||
"protected_session_timeout_description": "Time of inactivity before the session is cleared from browser memory. See",
|
||||
"wiki": "wiki",
|
||||
"for_more_info": "for more info.",
|
||||
"protected_session_timeout_label": "Protected session timeout:",
|
||||
"protected_session_timeout_label": "Auto-close session after",
|
||||
"reset_confirmation": "By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?",
|
||||
"reset_success_message": "Password has been reset. Please set new password",
|
||||
"change_password_heading": "Change Password",
|
||||
@@ -1530,18 +1569,17 @@
|
||||
"related_description": "Configure spell check languages and custom dictionary."
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "Sync Configuration",
|
||||
"server_address": "Server instance address",
|
||||
"timeout": "Sync timeout",
|
||||
"timeout_description": "How long to wait before giving up on a slow sync connection. Increase if you have an unstable network.",
|
||||
"proxy_label": "Sync proxy server (optional)",
|
||||
"note": "Note",
|
||||
"note_description": "If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only).",
|
||||
"special_value_description": "Another special value is <code>noproxy</code> which forces ignoring even the system proxy and respects <code>NODE_TLS_REJECT_UNAUTHORIZED</code>.",
|
||||
"config_title": "Sync Server",
|
||||
"server_address": "Server address",
|
||||
"server_address_description": "URL of the Trilium server to sync with.",
|
||||
"timeout": "Connection timeout",
|
||||
"timeout_description": "Time to wait before giving up on a slow connection.",
|
||||
"proxy_label": "Proxy server",
|
||||
"proxy_description": "Leave blank to use system proxy (desktop only). Use \"noproxy\" to bypass all proxies.",
|
||||
"save": "Save",
|
||||
"help": "Help",
|
||||
"test_title": "Sync Test",
|
||||
"test_description": "This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.",
|
||||
"test_title": "Test Connection",
|
||||
"test_description": "Test the connection to the sync server. If not initialized, this will set up syncing.",
|
||||
"test_button": "Test sync",
|
||||
"handshake_failed": "Sync server handshake failed, error: {{message}}"
|
||||
},
|
||||
@@ -1903,6 +1941,7 @@
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "Formatting toolbar",
|
||||
"toolbar_style": "Toolbar style",
|
||||
"floating": {
|
||||
"title": "Floating",
|
||||
"description": "editing tools appear near the cursor;"
|
||||
@@ -1911,7 +1950,7 @@
|
||||
"title": "Fixed",
|
||||
"description": "editing tools appear in the \"Formatting\" ribbon tab."
|
||||
},
|
||||
"multiline-toolbar": "Display the toolbar on multiple lines if it doesn't fit."
|
||||
"multiline-toolbar": "Display the toolbar on multiple lines if it doesn't fit"
|
||||
}
|
||||
},
|
||||
"electron_context_menu": {
|
||||
@@ -1978,7 +2017,7 @@
|
||||
"days": "Days"
|
||||
},
|
||||
"share": {
|
||||
"title": "Share Settings",
|
||||
"title": "Share",
|
||||
"redirect_bare_domain": "Redirect bare domain to Share page",
|
||||
"redirect_bare_domain_description": "Redirect anonymous users to the Share page instead of showing Login",
|
||||
"show_login_link": "Show Login link in Share theme",
|
||||
@@ -2042,12 +2081,12 @@
|
||||
},
|
||||
"editorfeatures": {
|
||||
"title": "Features",
|
||||
"emoji_completion_enabled": "Enable Emoji auto-completion",
|
||||
"emoji_completion_description": "If enabled, emojis can be easily inserted into text by typing `:`, followed by the name of an emoji.",
|
||||
"note_completion_enabled": "Enable note auto-completion",
|
||||
"note_completion_description": "If enabled, links to notes can be created by typing `@` followed by the title of a note.",
|
||||
"slash_commands_enabled": "Enable slash commands",
|
||||
"slash_commands_description": "If enabled, editing commands such as inserting line breaks or headings can be toggled by typing `/`."
|
||||
"emoji_completion_enabled": "Emoji auto-completion",
|
||||
"emoji_completion_description": "Emojis can be easily inserted into text by typing `:`, followed by the name of an emoji.",
|
||||
"note_completion_enabled": "Note auto-completion",
|
||||
"note_completion_description": "Links to notes can be created by typing `@` followed by the title of a note.",
|
||||
"slash_commands_enabled": "Slash commands",
|
||||
"slash_commands_description": "Editing commands such as inserting line breaks or headings can be toggled by typing `/`."
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "New row",
|
||||
@@ -2166,6 +2205,8 @@
|
||||
"related_code_blocks": "Color scheme for code blocks in text notes",
|
||||
"related_code_notes": "Color scheme for code notes",
|
||||
"ui": "User interface",
|
||||
"ui_layout_style": "Layout style",
|
||||
"ui_layout_orientation": "Launcher bar orientation",
|
||||
"ui_old_layout": "Old layout",
|
||||
"ui_new_layout": "New layout"
|
||||
},
|
||||
|
||||
@@ -24,14 +24,15 @@ interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
|
||||
onChange: OnChangeListener;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combobox component that takes in any object array as data. Each item of the array is rendered as an item, and the key and values are obtained by looking into the object by a specified key.
|
||||
*/
|
||||
export default function FormSelect<T>({ name, id, onChange, style, className, ...restProps }: FormSelectProps<T, T>) {
|
||||
export default function FormSelect<T>({ name, id, onChange, style, className, disabled, ...restProps }: FormSelectProps<T, T>) {
|
||||
return (
|
||||
<FormSelectBody name={name} id={id} onChange={onChange} style={style} className={className}>
|
||||
<FormSelectBody name={name} id={id} onChange={onChange} style={style} className={className} disabled={disabled}>
|
||||
<FormSelectGroup {...restProps} />
|
||||
</FormSelectBody>
|
||||
);
|
||||
@@ -40,9 +41,9 @@ export default function FormSelect<T>({ name, id, onChange, style, className, ..
|
||||
/**
|
||||
* Similar to {@link FormSelect}, but the top-level elements are actually groups.
|
||||
*/
|
||||
export function FormSelectWithGroups<T>({ name, id, values, keyProperty, titleProperty, currentValue, onChange, ...restProps }: FormSelectProps<T, FormSelectGroup<T> | T>) {
|
||||
export function FormSelectWithGroups<T>({ name, id, values, keyProperty, titleProperty, currentValue, onChange, disabled, ...restProps }: FormSelectProps<T, FormSelectGroup<T> | T>) {
|
||||
return (
|
||||
<FormSelectBody name={name} id={id} onChange={onChange} {...restProps}>
|
||||
<FormSelectBody name={name} id={id} onChange={onChange} disabled={disabled} {...restProps}>
|
||||
{values.map((item) => {
|
||||
if (!item) return <></>;
|
||||
if (typeof item === "object" && "items" in item) {
|
||||
@@ -61,7 +62,7 @@ export function FormSelectWithGroups<T>({ name, id, values, keyProperty, titlePr
|
||||
)
|
||||
}
|
||||
|
||||
function FormSelectBody({ id, name, children, onChange, style, className }: { id?: string, name?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties, className?: string }) {
|
||||
function FormSelectBody({ id, name, children, onChange, style, className, disabled }: { id?: string, name?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties, className?: string, disabled?: boolean }) {
|
||||
return (
|
||||
<select
|
||||
id={id}
|
||||
@@ -69,6 +70,7 @@ function FormSelectBody({ id, name, children, onChange, style, className }: { id
|
||||
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
||||
style={style}
|
||||
className={`form-select ${className ?? ""}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
import { AnonymizedDbResponse, DatabaseAnonymizeResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
|
||||
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { experimentalFeatures, type ExperimentalFeatureId } from "../../../services/experimental_features";
|
||||
import { type ExperimentalFeatureId,experimentalFeatures } from "../../../services/experimental_features";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import Button from "../../react/Button";
|
||||
import Column from "../../react/Column";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOptionJson } from "../../react/hooks";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import { OptionsRowWithButton, OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
|
||||
export default function AdvancedSettings() {
|
||||
return <>
|
||||
<AdvancedSyncOptions />
|
||||
<DatabaseIntegrityOptions />
|
||||
<DatabaseOptions />
|
||||
<DatabaseAnonymizationOptions />
|
||||
<VacuumDatabaseOptions />
|
||||
<ExperimentalOptions />
|
||||
<AdvancedSyncOptions />
|
||||
</>;
|
||||
}
|
||||
|
||||
function AdvancedSyncOptions() {
|
||||
return (
|
||||
<OptionsSection title={t("sync.title")}>
|
||||
<Button
|
||||
text={t("sync.force_full_sync_button")}
|
||||
<OptionsRowWithButton
|
||||
label={t("sync.force_full_sync_label")}
|
||||
description={t("sync.force_full_sync_description")}
|
||||
onClick={async () => {
|
||||
await server.post("sync/force-full-sync");
|
||||
toast.showMessage(t("sync.full_sync_triggered"));
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
text={t("sync.fill_entity_changes_button")}
|
||||
<OptionsRowWithButton
|
||||
label={t("sync.fill_entity_changes_label")}
|
||||
description={t("sync.fill_entity_changes_description")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("sync.filling_entity_changes"));
|
||||
await server.post("sync/fill-entity-changes");
|
||||
@@ -46,13 +44,12 @@ function AdvancedSyncOptions() {
|
||||
);
|
||||
}
|
||||
|
||||
function DatabaseIntegrityOptions() {
|
||||
function DatabaseOptions() {
|
||||
return (
|
||||
<OptionsSection title={t("database_integrity_check.title")}>
|
||||
<FormText>{t("database_integrity_check.description")}</FormText>
|
||||
|
||||
<Button
|
||||
text={t("database_integrity_check.check_button")}
|
||||
<OptionsSection title={t("database.title")}>
|
||||
<OptionsRowWithButton
|
||||
label={t("database_integrity_check.check_integrity_label")}
|
||||
description={t("database_integrity_check.check_integrity_description")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("database_integrity_check.checking_integrity"));
|
||||
|
||||
@@ -66,20 +63,31 @@ function DatabaseIntegrityOptions() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
text={t("consistency_checks.find_and_fix_button")}
|
||||
<OptionsRowWithButton
|
||||
label={t("consistency_checks.find_and_fix_label")}
|
||||
description={t("consistency_checks.find_and_fix_description")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("consistency_checks.finding_and_fixing_message"));
|
||||
await server.post("database/find-and-fix-consistency-issues");
|
||||
toast.showMessage(t("consistency_checks.issues_fixed_message"));
|
||||
}}
|
||||
/>
|
||||
|
||||
<OptionsRowWithButton
|
||||
label={t("vacuum_database.vacuum_label")}
|
||||
description={t("vacuum_database.vacuum_description")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("vacuum_database.vacuuming_database"));
|
||||
await server.post("database/vacuum-database");
|
||||
toast.showMessage(t("vacuum_database.database_vacuumed"));
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function DatabaseAnonymizationOptions() {
|
||||
const [ existingAnonymizedDatabases, setExistingAnonymizedDatabases ] = useState<AnonymizedDbResponse[]>([]);
|
||||
const [existingAnonymizedDatabases, setExistingAnonymizedDatabases] = useState<AnonymizedDbResponse[]>([]);
|
||||
|
||||
function refreshAnonymizedDatabase() {
|
||||
server.get<AnonymizedDbResponse[]>("database/anonymized-databases").then(setExistingAnonymizedDatabases);
|
||||
@@ -89,59 +97,47 @@ function DatabaseAnonymizationOptions() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("database_anonymization.title")}>
|
||||
<FormText>{t("database_anonymization.choose_anonymization")}</FormText>
|
||||
<FormText>{t("database_anonymization.description")}</FormText>
|
||||
|
||||
<div className="row">
|
||||
<DatabaseAnonymizationOption
|
||||
title={t("database_anonymization.full_anonymization")}
|
||||
description={t("database_anonymization.full_anonymization_description")}
|
||||
buttonText={t("database_anonymization.save_fully_anonymized_database")}
|
||||
buttonClick={async () => {
|
||||
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
|
||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
|
||||
<OptionsRowWithButton
|
||||
label={t("database_anonymization.full_anonymization")}
|
||||
description={t("database_anonymization.full_anonymization_description")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
|
||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
|
||||
|
||||
if (!resp.success) {
|
||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toast.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
refreshAnonymizedDatabase();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DatabaseAnonymizationOption
|
||||
title={t("database_anonymization.light_anonymization")}
|
||||
description={t("database_anonymization.light_anonymization_description")}
|
||||
buttonText={t("database_anonymization.save_lightly_anonymized_database")}
|
||||
buttonClick={async () => {
|
||||
toast.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
|
||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/light");
|
||||
if (!resp.success) {
|
||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toast.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
refreshAnonymizedDatabase();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
if (!resp.success) {
|
||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toast.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
refreshAnonymizedDatabase();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<OptionsRowWithButton
|
||||
label={t("database_anonymization.light_anonymization")}
|
||||
description={t("database_anonymization.light_anonymization_description")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
|
||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/light");
|
||||
|
||||
if (!resp.success) {
|
||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toast.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
refreshAnonymizedDatabase();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<ExistingAnonymizedDatabases databases={existingAnonymizedDatabases} />
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function DatabaseAnonymizationOption({ title, description, buttonText, buttonClick }: { title: string, description: string, buttonText: string, buttonClick: () => void }) {
|
||||
return (
|
||||
<Column md={6} style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginTop: "1em" }}>
|
||||
<h5>{title}</h5>
|
||||
<FormText>{description}</FormText>
|
||||
<Button text={buttonText} onClick={buttonClick} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbResponse[] }) {
|
||||
if (!databases.length) {
|
||||
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>;
|
||||
@@ -163,22 +159,6 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
|
||||
);
|
||||
}
|
||||
|
||||
function VacuumDatabaseOptions() {
|
||||
return (
|
||||
<OptionsSection title={t("vacuum_database.title")}>
|
||||
<FormText>{t("vacuum_database.description")}</FormText>
|
||||
|
||||
<Button
|
||||
text={t("vacuum_database.button_text")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("vacuum_database.vacuuming_database"));
|
||||
await server.post("database/vacuum-database");
|
||||
toast.showMessage(t("vacuum_database.database_vacuumed"));
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function ExperimentalOptions() {
|
||||
const [enabledFeatures, setEnabledFeatures] = useTriliumOptionJson<ExperimentalFeatureId[]>("experimentalFeatures", true);
|
||||
@@ -201,18 +181,14 @@ function ExperimentalOptions() {
|
||||
<FormText>{t("experimental_features.disclaimer")}</FormText>
|
||||
|
||||
{filteredFeatures.map((feature) => (
|
||||
<OptionsRow
|
||||
<OptionsRowWithToggle
|
||||
key={feature.id}
|
||||
name={`experimental-${feature.id}`}
|
||||
label={feature.name}
|
||||
description={feature.description}
|
||||
>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={enabledFeatures.includes(feature.id)}
|
||||
onChange={(enabled) => toggleFeature(feature.id, enabled)}
|
||||
/>
|
||||
</OptionsRow>
|
||||
currentValue={enabledFeatures.includes(feature.id)}
|
||||
onChange={(enabled) => toggleFeature(feature.id, enabled)}
|
||||
/>
|
||||
))}
|
||||
</OptionsSection>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,234 @@
|
||||
/* Font Option Row */
|
||||
.font-option-row {
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.font-option-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
/* Font Picker Modal */
|
||||
.font-picker-modal .modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.font-picker-content {
|
||||
display: flex;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.font-picker-list {
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--main-border-color);
|
||||
overflow: hidden;
|
||||
|
||||
.dropdownWrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-picker-settings {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.font-size-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.font-size-description {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-size-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.font-size-value {
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
.font-preview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.font-preview-text {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--main-background-color);
|
||||
overflow: auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.orientation-illustration {
|
||||
width: 170px;
|
||||
height: 130px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--root-background);
|
||||
overflow: hidden;
|
||||
|
||||
> .tab-bar.full-width {
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
padding: 0 4px;
|
||||
background: var(--launcher-pane-background-color);
|
||||
|
||||
.tab {
|
||||
width: 24px;
|
||||
height: 8px;
|
||||
background: var(--main-background-color);
|
||||
border-radius: 4px 4px 0 0;
|
||||
opacity: 0.4;
|
||||
|
||||
&.active {
|
||||
height: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.launcher-bar {
|
||||
background: var(--launcher-pane-vert-background-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.tn-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
height: 14px;
|
||||
padding: 0 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
width: 24px;
|
||||
padding: 4px 0;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-area {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tree-pane {
|
||||
width: 40px;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 4px;
|
||||
|
||||
.tree-content {
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
180deg,
|
||||
var(--main-text-color) 0px,
|
||||
var(--main-text-color) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
opacity: 0.15;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-pane {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tab-bar {
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
padding: 0 4px;
|
||||
|
||||
.tab {
|
||||
width: 24px;
|
||||
height: 8px;
|
||||
background: var(--main-background-color);
|
||||
border-radius: 4px 4px 0 0;
|
||||
opacity: 0.4;
|
||||
|
||||
&.active {
|
||||
height: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-content {
|
||||
flex-grow: 1;
|
||||
background: var(--main-background-color);
|
||||
border-top-left-radius: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.old-layout-illustration {
|
||||
width: 170px;
|
||||
height: 130px;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import "./appearance.css";
|
||||
|
||||
import { FontFamily, OptionNames } from "@triliumnext/commons";
|
||||
import { FontFamily, OptionNames, SYSTEM_MONOSPACE_FONT_STACK, SYSTEM_SANS_SERIF_FONT_STACK } from "@triliumnext/commons";
|
||||
import { Fragment } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import zoomService from "../../../components/zoom";
|
||||
@@ -8,17 +10,14 @@ import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils";
|
||||
import { VerticalLayoutIcon } from "../../buttons/global_menu";
|
||||
import Button from "../../react/Button";
|
||||
import Column from "../../react/Column";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import FormSelect, { FormSelectWithGroups } from "../../react/FormSelect";
|
||||
import FormText from "../../react/FormText";
|
||||
import Dropdown from "../../react/Dropdown";
|
||||
import FormList, { FormListHeader, FormListItem } from "../../react/FormList";
|
||||
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import Modal from "../../react/Modal";
|
||||
import Slider from "../../react/Slider";
|
||||
import OptionsRow, { OptionsRowWithButton, OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import PlatformIndicator from "./components/PlatformIndicator";
|
||||
import RadioWithIllustration from "./components/RadioWithIllustration";
|
||||
@@ -29,16 +28,20 @@ const MIN_CONTENT_WIDTH = 640;
|
||||
interface Theme {
|
||||
val: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
noteId?: string;
|
||||
}
|
||||
|
||||
const BUILTIN_THEMES: Theme[] = [
|
||||
{ val: "next", title: t("theme.triliumnext") },
|
||||
{ val: "next-light", title: t("theme.triliumnext-light") },
|
||||
{ val: "next-dark", title: t("theme.triliumnext-dark") },
|
||||
{ val: "auto", title: t("theme.auto_theme") },
|
||||
{ val: "light", title: t("theme.light_theme") },
|
||||
{ val: "dark", title: t("theme.dark_theme") }
|
||||
const MODERN_THEMES: Theme[] = [
|
||||
{ val: "next", title: t("theme.triliumnext"), icon: "bx bx-sun bx-flip-horizontal" },
|
||||
{ val: "next-light", title: t("theme.triliumnext-light"), icon: "bx bx-sun" },
|
||||
{ val: "next-dark", title: t("theme.triliumnext-dark"), icon: "bx bx-moon" }
|
||||
];
|
||||
|
||||
const LEGACY_THEMES: Theme[] = [
|
||||
{ val: "auto", title: t("theme.auto_theme"), icon: "bx bx-sun bx-flip-horizontal" },
|
||||
{ val: "light", title: t("theme.light_theme"), icon: "bx bx-sun" },
|
||||
{ val: "dark", title: t("theme.dark_theme"), icon: "bx bx-moon" }
|
||||
];
|
||||
|
||||
interface FontFamilyEntry {
|
||||
@@ -89,14 +92,10 @@ const FONT_FAMILIES: FontGroup[] = [
|
||||
];
|
||||
|
||||
export default function AppearanceSettings() {
|
||||
const [ overrideThemeFonts ] = useTriliumOption("overrideThemeFonts");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isMobile() && <LayoutSwitcher />}
|
||||
{!isMobile() && <LayoutOrientation />}
|
||||
<ApplicationTheme />
|
||||
{overrideThemeFonts === "true" && <Fonts />}
|
||||
<UserInterface />
|
||||
<Fonts />
|
||||
{isElectron() && <ElectronIntegration /> }
|
||||
<Performance />
|
||||
<MaxContentWidth />
|
||||
@@ -115,22 +114,97 @@ export default function AppearanceSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutSwitcher() {
|
||||
function UserInterface() {
|
||||
const [ theme, setTheme ] = useTriliumOption("theme", true);
|
||||
const [ customThemes, setCustomThemes ] = useState<Theme[]>([]);
|
||||
const [ newLayout, setNewLayout ] = useTriliumOptionBool("newLayout");
|
||||
const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation", true);
|
||||
|
||||
useEffect(() => {
|
||||
server.get<Theme[]>("options/user-themes").then((userThemes) => {
|
||||
setCustomThemes(userThemes);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Find current theme for display
|
||||
const allThemes = [...MODERN_THEMES, ...LEGACY_THEMES, ...customThemes];
|
||||
const currentTheme = allThemes.find(t => t.val === theme);
|
||||
const currentThemeIcon = currentTheme?.icon ?? "bx bx-palette";
|
||||
const currentThemeLabel = currentTheme?.title ?? theme ?? "";
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("settings_appearance.ui")}>
|
||||
<RadioWithIllustration
|
||||
currentValue={newLayout ? "new-layout" : "old-layout"}
|
||||
onChange={async newValue => {
|
||||
await setNewLayout(newValue === "new-layout");
|
||||
reloadFrontendApp();
|
||||
}}
|
||||
values={[
|
||||
{ key: "old-layout", text: t("settings_appearance.ui_old_layout"), illustration: <LayoutIllustration /> },
|
||||
{ key: "new-layout", text: t("settings_appearance.ui_new_layout"), illustration: <LayoutIllustration isNewLayout /> }
|
||||
]}
|
||||
/>
|
||||
<OptionsSection title={t("theme.title")}>
|
||||
<OptionsRow name="theme" label={t("theme.theme_label")}>
|
||||
<Dropdown
|
||||
text={<>
|
||||
<span className={currentThemeIcon} style={{ marginRight: "8px" }} />
|
||||
{currentThemeLabel}
|
||||
</>}
|
||||
>
|
||||
<FormListHeader text={t("theme.modern_themes")} />
|
||||
{MODERN_THEMES.map(th => (
|
||||
<FormListItem
|
||||
key={th.val}
|
||||
icon={th.icon}
|
||||
selected={theme === th.val}
|
||||
onClick={() => setTheme(th.val)}
|
||||
>
|
||||
{th.title}
|
||||
</FormListItem>
|
||||
))}
|
||||
<FormListHeader text={t("theme.legacy_themes")} />
|
||||
{LEGACY_THEMES.map(th => (
|
||||
<FormListItem
|
||||
key={th.val}
|
||||
icon={th.icon}
|
||||
selected={theme === th.val}
|
||||
onClick={() => setTheme(th.val)}
|
||||
>
|
||||
{th.title}
|
||||
</FormListItem>
|
||||
))}
|
||||
{customThemes.length > 0 && (
|
||||
<>
|
||||
<FormListHeader text={t("theme.custom_themes")} />
|
||||
{customThemes.map(ct => (
|
||||
<FormListItem
|
||||
key={ct.val}
|
||||
icon={ct.icon}
|
||||
selected={theme === ct.val}
|
||||
onClick={() => setTheme(ct.val)}
|
||||
>
|
||||
{ct.title}
|
||||
</FormListItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Dropdown>
|
||||
</OptionsRow>
|
||||
{!isMobile() && <>
|
||||
<OptionsRow name="layout-style" label={t("settings_appearance.ui_layout_style")}>
|
||||
<RadioWithIllustration
|
||||
currentValue={newLayout ? "new-layout" : "old-layout"}
|
||||
onChange={async newValue => {
|
||||
await setNewLayout(newValue === "new-layout");
|
||||
reloadFrontendApp();
|
||||
}}
|
||||
values={[
|
||||
{ key: "old-layout", text: t("settings_appearance.ui_old_layout"), illustration: <LayoutIllustration /> },
|
||||
{ key: "new-layout", text: t("settings_appearance.ui_new_layout"), illustration: <LayoutIllustration isNewLayout /> }
|
||||
]}
|
||||
/>
|
||||
</OptionsRow>
|
||||
<OptionsRow name="layout-orientation" label={t("settings_appearance.ui_layout_orientation")}>
|
||||
<RadioWithIllustration
|
||||
currentValue={layoutOrientation ?? "vertical"}
|
||||
onChange={setLayoutOrientation}
|
||||
values={[
|
||||
{ key: "vertical", text: t("theme.layout-vertical-title"), illustration: <OrientationIllustration orientation="vertical" /> },
|
||||
{ key: "horizontal", text: t("theme.layout-horizontal-title"), illustration: <OrientationIllustration orientation="horizontal" /> }
|
||||
]}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</>}
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
@@ -227,113 +301,237 @@ function LayoutIllustration({ isNewLayout }: { isNewLayout?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutOrientation() {
|
||||
const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation", true);
|
||||
function OrientationIllustration({ orientation }: { orientation: "vertical" | "horizontal" }) {
|
||||
const isHorizontal = orientation === "horizontal";
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("theme.layout")}>
|
||||
<FormRadioGroup
|
||||
name="layout-orientation"
|
||||
values={[
|
||||
{
|
||||
label: t("theme.layout-vertical-title"),
|
||||
inlineDescription: t("theme.layout-vertical-description"),
|
||||
value: "vertical"
|
||||
},
|
||||
{
|
||||
label: t("theme.layout-horizontal-title"),
|
||||
inlineDescription: t("theme.layout-horizontal-description"),
|
||||
value: "horizontal"
|
||||
}
|
||||
]}
|
||||
currentValue={layoutOrientation} onChange={setLayoutOrientation}
|
||||
<div className={`orientation-illustration ${orientation}`}>
|
||||
{isHorizontal && (
|
||||
<div className="tab-bar full-width">
|
||||
<div className="tab active" />
|
||||
<div className="tab" />
|
||||
<div className="tab" />
|
||||
</div>
|
||||
)}
|
||||
{isHorizontal && (
|
||||
<div className="launcher-bar horizontal">
|
||||
<Icon icon="bx bx-menu" />
|
||||
<Icon icon="bx bx-send" />
|
||||
<Icon icon="bx bx-file-blank" />
|
||||
<Icon icon="bx bx-search" />
|
||||
</div>
|
||||
)}
|
||||
<div className="main-area">
|
||||
{!isHorizontal && (
|
||||
<div className="launcher-bar vertical">
|
||||
<Icon icon="bx bx-menu" />
|
||||
<Icon icon="bx bx-send" />
|
||||
<Icon icon="bx bx-file-blank" />
|
||||
<Icon icon="bx bx-search" />
|
||||
</div>
|
||||
)}
|
||||
<div className="tree-pane">
|
||||
<div className="tree-content" />
|
||||
</div>
|
||||
<div className="content-pane">
|
||||
{!isHorizontal && (
|
||||
<div className="tab-bar">
|
||||
<div className="tab active" />
|
||||
<div className="tab" />
|
||||
<div className="tab" />
|
||||
</div>
|
||||
)}
|
||||
<div className="note-content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Fonts() {
|
||||
const [ overrideThemeFonts, setOverrideThemeFonts ] = useTriliumOptionBool("overrideThemeFonts");
|
||||
const isEnabled = overrideThemeFonts === true;
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("fonts.fonts")}>
|
||||
<OptionsRowWithToggle
|
||||
name="override-theme-fonts"
|
||||
label={t("fonts.custom_fonts")}
|
||||
description={t("fonts.not_all_fonts_available")}
|
||||
currentValue={overrideThemeFonts}
|
||||
onChange={setOverrideThemeFonts}
|
||||
/>
|
||||
|
||||
<Font label={t("fonts.main_font")} fontFamilyOption="mainFontFamily" fontSizeOption="mainFontSize" disabled={!isEnabled} />
|
||||
<Font label={t("fonts.note_tree_font")} sizeDescription={t("fonts.size_relative_to_general")} fontFamilyOption="treeFontFamily" fontSizeOption="treeFontSize" disabled={!isEnabled} />
|
||||
<Font label={t("fonts.note_detail_font")} sizeDescription={t("fonts.size_relative_to_general")} fontFamilyOption="detailFontFamily" fontSizeOption="detailFontSize" disabled={!isEnabled} />
|
||||
<Font label={t("fonts.monospace_font")} description={t("fonts.monospace_font_description")} fontFamilyOption="monospaceFontFamily" fontSizeOption="monospaceFontSize" disabled={!isEnabled} isMonospace />
|
||||
|
||||
<OptionsRowWithButton
|
||||
label={t("fonts.apply_changes")}
|
||||
icon="bx bx-refresh"
|
||||
onClick={reloadFrontendApp}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function ApplicationTheme() {
|
||||
const [ theme, setTheme ] = useTriliumOption("theme", true);
|
||||
const [ overrideThemeFonts, setOverrideThemeFonts ] = useTriliumOptionBool("overrideThemeFonts");
|
||||
|
||||
const [ themes, setThemes ] = useState<Theme[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
server.get<Theme[]>("options/user-themes").then((userThemes) => {
|
||||
setThemes([
|
||||
...BUILTIN_THEMES,
|
||||
...userThemes
|
||||
]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("theme.title")}>
|
||||
<div className="row">
|
||||
<FormGroup name="theme" label={t("theme.theme_label")} className="col-md-6" style={{ marginBottom: 0 }}>
|
||||
<FormSelect
|
||||
values={themes} currentValue={theme} onChange={setTheme}
|
||||
keyProperty="val" titleProperty="title"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className="side-checkbox col-md-6" name="override-theme-fonts">
|
||||
<FormCheckbox
|
||||
label={t("theme.override_theme_fonts_label")}
|
||||
currentValue={overrideThemeFonts} onChange={setOverrideThemeFonts} />
|
||||
</FormGroup>
|
||||
</div>
|
||||
</OptionsSection>
|
||||
);
|
||||
interface FontProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
sizeDescription?: string;
|
||||
fontFamilyOption: OptionNames;
|
||||
fontSizeOption: OptionNames;
|
||||
disabled?: boolean;
|
||||
isMonospace?: boolean;
|
||||
}
|
||||
|
||||
function Fonts() {
|
||||
return (
|
||||
<OptionsSection title={t("fonts.fonts")}>
|
||||
<Font title={t("fonts.main_font")} fontFamilyOption="mainFontFamily" fontSizeOption="mainFontSize" />
|
||||
<Font title={t("fonts.note_tree_font")} fontFamilyOption="treeFontFamily" fontSizeOption="treeFontSize" />
|
||||
<Font title={t("fonts.note_detail_font")} fontFamilyOption="detailFontFamily" fontSizeOption="detailFontSize" />
|
||||
<Font title={t("fonts.monospace_font")} fontFamilyOption="monospaceFontFamily" fontSizeOption="monospaceFontSize" />
|
||||
|
||||
<FormText>{t("fonts.note_tree_and_detail_font_sizing")}</FormText>
|
||||
<FormText>{t("fonts.not_all_fonts_available")}</FormText>
|
||||
|
||||
<p>
|
||||
{t("fonts.apply_font_changes")} <Button text={t("fonts.reload_frontend")} size="micro" onClick={reloadFrontendApp} />
|
||||
</p>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function Font({ title, fontFamilyOption, fontSizeOption }: { title: string, fontFamilyOption: OptionNames, fontSizeOption: OptionNames }) {
|
||||
function Font({ label, description, sizeDescription, fontFamilyOption, fontSizeOption, disabled, isMonospace }: FontProps) {
|
||||
const [ fontFamily, setFontFamily ] = useTriliumOption(fontFamilyOption);
|
||||
const [ fontSize, setFontSize ] = useTriliumOption(fontSizeOption);
|
||||
const [ showModal, setShowModal ] = useState(false);
|
||||
|
||||
// Find the current font entry to display
|
||||
const currentFont = FONT_FAMILIES
|
||||
.flatMap(group => group.items)
|
||||
.find(item => item.value === fontFamily);
|
||||
const displayLabel = currentFont?.label ?? currentFont?.value ?? fontFamily ?? "";
|
||||
|
||||
// Map option name to CSS variable
|
||||
const themeCssVariable = {
|
||||
mainFontFamily: "var(--main-font-family)",
|
||||
treeFontFamily: "var(--tree-font-family)",
|
||||
detailFontFamily: "var(--detail-font-family)",
|
||||
monospaceFontFamily: "var(--monospace-font-family)"
|
||||
}[fontFamilyOption] ?? "inherit";
|
||||
|
||||
// Get the CSS font-family value for preview
|
||||
const getFontFamily = (value: string) => {
|
||||
if (value === "theme") {
|
||||
// Use the theme's CSS variable for this font option
|
||||
return themeCssVariable;
|
||||
}
|
||||
if (value === "system") {
|
||||
// Use the appropriate system font stack
|
||||
return isMonospace ? SYSTEM_MONOSPACE_FONT_STACK : SYSTEM_SANS_SERIF_FONT_STACK;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5>{title}</h5>
|
||||
<div className="row">
|
||||
<FormGroup name="font-family" className="col-md-4" label={t("fonts.font_family")}>
|
||||
<FormSelectWithGroups
|
||||
values={FONT_FAMILIES}
|
||||
currentValue={fontFamily} onChange={setFontFamily}
|
||||
keyProperty="value" titleProperty="label"
|
||||
/>
|
||||
</FormGroup>
|
||||
<button
|
||||
type="button"
|
||||
className="option-row option-row-link font-option-row"
|
||||
onClick={() => setShowModal(true)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="option-row-label">
|
||||
<label style={{ cursor: "pointer" }}>{label}</label>
|
||||
{description && <small>{description}</small>}
|
||||
</div>
|
||||
<div className="option-row-input font-option-preview">
|
||||
<span style={{ fontFamily: getFontFamily(fontFamily ?? ""), fontSize: `${fontSize}%` }}>{displayLabel}</span>
|
||||
<span className="bx bx-chevron-right" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<FormGroup name="font-size" className="col-md-6" label={t("fonts.size")}>
|
||||
<FormTextBoxWithUnit
|
||||
name="tree-font-size"
|
||||
type="number" min={50} max={200} step={10}
|
||||
currentValue={fontSize} onBlur={setFontSize}
|
||||
unit={t("units.percentage")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<FontPickerModal
|
||||
show={showModal}
|
||||
onHidden={() => setShowModal(false)}
|
||||
title={label}
|
||||
fontFamily={fontFamily ?? ""}
|
||||
fontSize={parseInt(fontSize ?? "100", 10)}
|
||||
onFontFamilyChange={setFontFamily}
|
||||
onFontSizeChange={(size) => setFontSize(String(size))}
|
||||
getFontFamily={getFontFamily}
|
||||
sizeDescription={sizeDescription}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog. 0123456789";
|
||||
|
||||
interface FontPickerModalProps {
|
||||
show: boolean;
|
||||
onHidden: () => void;
|
||||
title: string;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
onFontFamilyChange: (value: string) => void;
|
||||
onFontSizeChange: (value: number) => void;
|
||||
getFontFamily: (value: string) => string | undefined;
|
||||
sizeDescription?: string;
|
||||
}
|
||||
|
||||
function FontPickerModal({ show, onHidden, title, fontFamily, fontSize, onFontFamilyChange, onFontSizeChange, getFontFamily, sizeDescription }: FontPickerModalProps) {
|
||||
return createPortal(
|
||||
<Modal
|
||||
className="font-picker-modal"
|
||||
title={title}
|
||||
size="lg"
|
||||
show={show}
|
||||
onHidden={onHidden}
|
||||
>
|
||||
<div className="font-picker-content">
|
||||
<div className="font-picker-list">
|
||||
<FormList fullHeight>
|
||||
{FONT_FAMILIES.map(group => (
|
||||
<Fragment key={group.title}>
|
||||
<FormListHeader text={group.title} />
|
||||
{group.items.map(item => (
|
||||
<FormListItem
|
||||
key={item.value}
|
||||
onClick={() => onFontFamilyChange(item.value)}
|
||||
checked={fontFamily === item.value}
|
||||
selected={fontFamily === item.value}
|
||||
>
|
||||
<span style={{ fontFamily: getFontFamily(item.value) }}>
|
||||
{item.label ?? item.value}
|
||||
</span>
|
||||
</FormListItem>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</FormList>
|
||||
</div>
|
||||
|
||||
<div className="font-picker-settings">
|
||||
<div className="font-size-control">
|
||||
<label>{t("fonts.size")}</label>
|
||||
<div className="font-size-slider">
|
||||
<Slider
|
||||
value={fontSize}
|
||||
onChange={onFontSizeChange}
|
||||
min={50}
|
||||
max={200}
|
||||
step={5}
|
||||
/>
|
||||
<span className="font-size-value">{fontSize}%</span>
|
||||
</div>
|
||||
{sizeDescription && <small className="font-size-description">{sizeDescription}</small>}
|
||||
</div>
|
||||
|
||||
<div className="font-preview">
|
||||
<label>{t("fonts.preview")}</label>
|
||||
<div
|
||||
className="font-preview-text"
|
||||
style={{
|
||||
fontFamily: getFontFamily(fontFamily),
|
||||
fontSize: `${fontSize}%`
|
||||
}}
|
||||
>
|
||||
{PREVIEW_TEXT}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
function ElectronIntegration() {
|
||||
const [ zoomFactor ] = useTriliumOption("zoomFactor");
|
||||
const [ nativeTitleBarVisible, setNativeTitleBarVisible ] = useTriliumOptionBool("nativeTitleBarVisible");
|
||||
@@ -353,26 +551,28 @@ function ElectronIntegration() {
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<FormGroup name="native-title-bar" description={t("electron_integration.native-title-bar-description")}>
|
||||
<FormCheckbox
|
||||
label={t("electron_integration.native-title-bar")}
|
||||
currentValue={nativeTitleBarVisible} onChange={setNativeTitleBarVisible}
|
||||
/>
|
||||
</FormGroup>
|
||||
<OptionsRowWithToggle
|
||||
name="native-title-bar"
|
||||
label={t("electron_integration.native-title-bar")}
|
||||
description={t("electron_integration.native-title-bar-description")}
|
||||
currentValue={nativeTitleBarVisible}
|
||||
onChange={setNativeTitleBarVisible}
|
||||
/>
|
||||
|
||||
<FormGroup name="background-effects" description={t("electron_integration.background-effects-description")}>
|
||||
<FormCheckbox
|
||||
label={<>
|
||||
{t("electron_integration.background-effects")}
|
||||
{" "}
|
||||
<PlatformIndicator windows="11" mac />
|
||||
</>}
|
||||
currentValue={backgroundEffects} onChange={setBackgroundEffects}
|
||||
disabled={nativeTitleBarVisible}
|
||||
/>
|
||||
</FormGroup>
|
||||
<OptionsRowWithToggle
|
||||
name="background-effects"
|
||||
label={<>{t("electron_integration.background-effects")} <PlatformIndicator windows="11" mac /></>}
|
||||
description={t("electron_integration.background-effects-description")}
|
||||
currentValue={backgroundEffects}
|
||||
onChange={setBackgroundEffects}
|
||||
disabled={nativeTitleBarVisible}
|
||||
/>
|
||||
|
||||
<Button text={t("electron_integration.restart-app-button")} onClick={restartDesktopApp} />
|
||||
<OptionsRowWithButton
|
||||
label={t("electron_integration.restart-app-button")}
|
||||
icon="bx bx-refresh"
|
||||
onClick={restartDesktopApp}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
@@ -383,19 +583,25 @@ function Performance() {
|
||||
const [ backdropEffectsEnabled, setBackdropEffectsEnabled ] = useTriliumOptionBool("backdropEffectsEnabled");
|
||||
|
||||
return <OptionsSection title={t("ui-performance.title")}>
|
||||
<FormCheckbox
|
||||
<OptionsRowWithToggle
|
||||
name="motion-enabled"
|
||||
label={t("ui-performance.enable-motion")}
|
||||
currentValue={motionEnabled} onChange={setMotionEnabled}
|
||||
currentValue={motionEnabled}
|
||||
onChange={setMotionEnabled}
|
||||
/>
|
||||
|
||||
<FormCheckbox
|
||||
<OptionsRowWithToggle
|
||||
name="shadows-enabled"
|
||||
label={t("ui-performance.enable-shadows")}
|
||||
currentValue={shadowsEnabled} onChange={setShadowsEnabled}
|
||||
currentValue={shadowsEnabled}
|
||||
onChange={setShadowsEnabled}
|
||||
/>
|
||||
|
||||
{!isMobile() && <FormCheckbox
|
||||
{!isMobile() && <OptionsRowWithToggle
|
||||
name="backdrop-effects-enabled"
|
||||
label={t("ui-performance.enable-backdrop-effects")}
|
||||
currentValue={backdropEffectsEnabled} onChange={setBackdropEffectsEnabled}
|
||||
currentValue={backdropEffectsEnabled}
|
||||
onChange={setBackdropEffectsEnabled}
|
||||
/>}
|
||||
|
||||
{isElectron() && <SmoothScrollEnabledOption />}
|
||||
@@ -406,9 +612,12 @@ function Performance() {
|
||||
function SmoothScrollEnabledOption() {
|
||||
const [ smoothScrollEnabled, setSmoothScrollEnabled ] = useTriliumOptionBool("smoothScrollEnabled");
|
||||
|
||||
return <FormCheckbox
|
||||
label={`${t("ui-performance.enable-smooth-scroll")} ${t("ui-performance.app-restart-required")}`}
|
||||
currentValue={smoothScrollEnabled} onChange={setSmoothScrollEnabled}
|
||||
return <OptionsRowWithToggle
|
||||
name="smooth-scroll-enabled"
|
||||
label={t("ui-performance.enable-smooth-scroll")}
|
||||
description={t("ui-performance.app-restart-required")}
|
||||
currentValue={smoothScrollEnabled}
|
||||
onChange={setSmoothScrollEnabled}
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -417,22 +626,21 @@ function MaxContentWidth() {
|
||||
const [centerContent, setCenterContent] = useTriliumOptionBool("centerContent");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("max_content_width.title")}>
|
||||
<FormText>{t("max_content_width.default_description")}</FormText>
|
||||
<OptionsSection title={t("max_content_width.title")} description={t("max_content_width.default_description")}>
|
||||
<OptionsRow name="max-content-width" label={t("max_content_width.max_width_label")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min={MIN_CONTENT_WIDTH} step="10"
|
||||
currentValue={maxContentWidth} onBlur={setMaxContentWidth}
|
||||
unit={t("max_content_width.max_width_unit")}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<Column md={6}>
|
||||
<FormGroup name="max-content-width" label={t("max_content_width.max_width_label")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min={MIN_CONTENT_WIDTH} step="10"
|
||||
currentValue={maxContentWidth} onBlur={setMaxContentWidth}
|
||||
unit={t("max_content_width.max_width_unit")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Column>
|
||||
|
||||
<FormCheckbox label={t("max_content_width.centerContent")}
|
||||
<OptionsRowWithToggle
|
||||
name="center-content"
|
||||
label={t("max_content_width.centerContent")}
|
||||
currentValue={centerContent}
|
||||
onChange={setCenterContent} />
|
||||
onChange={setCenterContent}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
@@ -442,9 +650,11 @@ function RibbonOptions() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t('ribbon.widgets')}>
|
||||
<FormCheckbox
|
||||
<OptionsRowWithToggle
|
||||
name="edited-notes-open-in-ribbon"
|
||||
label={t('ribbon.edited_notes_message')}
|
||||
currentValue={editedNotesOpenInRibbon} onChange={setEditedNotesOpenInRibbon}
|
||||
currentValue={editedNotesOpenInRibbon}
|
||||
onChange={setEditedNotesOpenInRibbon}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
|
||||
@@ -5,15 +5,14 @@ import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { formatDateTime } from "../../../utils/formatters";
|
||||
import Button from "../../react/Button";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import { FormMultiGroup } from "../../react/FormGroup";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import FormText from "../../react/FormText";
|
||||
import { useTriliumOptionBool } from "../../react/hooks";
|
||||
import { OptionsRowWithButton, OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
|
||||
export default function BackupSettings() {
|
||||
const [ backups, setBackups ] = useState<DatabaseBackup[]>([]);
|
||||
const [backups, setBackups] = useState<DatabaseBackup[]>([]);
|
||||
|
||||
const refreshBackups = useCallback(() => {
|
||||
server.get<DatabaseBackup[]>("database/backups").then((backupFiles) => {
|
||||
@@ -26,56 +25,54 @@ export default function BackupSettings() {
|
||||
|
||||
setBackups(backupFiles);
|
||||
});
|
||||
}, [ setBackups ]);
|
||||
}, [setBackups]);
|
||||
|
||||
useEffect(refreshBackups, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutomaticBackup />
|
||||
<BackupNow refreshCallback={refreshBackups} />
|
||||
<BackupConfiguration refreshCallback={refreshBackups} />
|
||||
<BackupList backups={backups} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutomaticBackup() {
|
||||
const [ dailyBackupEnabled, setDailyBackupEnabled ] = useTriliumOptionBool("dailyBackupEnabled");
|
||||
const [ weeklyBackupEnabled, setWeeklyBackupEnabled ] = useTriliumOptionBool("weeklyBackupEnabled");
|
||||
const [ monthlyBackupEnabled, setMonthlyBackupEnabled ] = useTriliumOptionBool("monthlyBackupEnabled");
|
||||
export function BackupConfiguration({ refreshCallback }: { refreshCallback: () => void }) {
|
||||
const [dailyBackupEnabled, setDailyBackupEnabled] = useTriliumOptionBool("dailyBackupEnabled");
|
||||
const [weeklyBackupEnabled, setWeeklyBackupEnabled] = useTriliumOptionBool("weeklyBackupEnabled");
|
||||
const [monthlyBackupEnabled, setMonthlyBackupEnabled] = useTriliumOptionBool("monthlyBackupEnabled");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("backup.automatic_backup")}>
|
||||
<FormMultiGroup label={t("backup.automatic_backup_description")}>
|
||||
<FormCheckbox
|
||||
name="daily-backup-enabled"
|
||||
label={t("backup.enable_daily_backup")}
|
||||
currentValue={dailyBackupEnabled} onChange={setDailyBackupEnabled}
|
||||
/>
|
||||
<OptionsSection title={t("backup.title")}>
|
||||
<FormText>{t("backup.automatic_backup_description")}</FormText>
|
||||
|
||||
<FormCheckbox
|
||||
name="weekly-backup-enabled"
|
||||
label={t("backup.enable_weekly_backup")}
|
||||
currentValue={weeklyBackupEnabled} onChange={setWeeklyBackupEnabled}
|
||||
/>
|
||||
<OptionsRowWithToggle
|
||||
name="daily-backup-enabled"
|
||||
label={t("backup.enable_daily_backup")}
|
||||
currentValue={dailyBackupEnabled}
|
||||
onChange={setDailyBackupEnabled}
|
||||
/>
|
||||
|
||||
<FormCheckbox
|
||||
name="monthly-backup-enabled"
|
||||
label={t("backup.enable_monthly_backup")}
|
||||
currentValue={monthlyBackupEnabled} onChange={setMonthlyBackupEnabled}
|
||||
/>
|
||||
</FormMultiGroup>
|
||||
<OptionsRowWithToggle
|
||||
name="weekly-backup-enabled"
|
||||
label={t("backup.enable_weekly_backup")}
|
||||
currentValue={weeklyBackupEnabled}
|
||||
onChange={setWeeklyBackupEnabled}
|
||||
/>
|
||||
|
||||
<OptionsRowWithToggle
|
||||
name="monthly-backup-enabled"
|
||||
label={t("backup.enable_monthly_backup")}
|
||||
currentValue={monthlyBackupEnabled}
|
||||
onChange={setMonthlyBackupEnabled}
|
||||
/>
|
||||
|
||||
<FormText>{t("backup.backup_recommendation")}</FormText>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
export function BackupNow({ refreshCallback }: { refreshCallback: () => void }) {
|
||||
return (
|
||||
<OptionsSection title={t("backup.backup_now")}>
|
||||
<Button
|
||||
text={t("backup.backup_database_now")}
|
||||
<hr />
|
||||
|
||||
<OptionsRowWithButton
|
||||
label={t("backup.backup_database_now")}
|
||||
onClick={async () => {
|
||||
const { backupFile } = await server.post<BackupDatabaseNowResponse>("database/backup-database");
|
||||
toast.showMessage(t("backup.database_backed_up_to", { backupFilePath: backupFile }), 10000);
|
||||
@@ -110,7 +107,7 @@ export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
|
||||
<td className="selectable-text">{filePath}</td>
|
||||
<td>
|
||||
<a href={`api/database/backup/download?filePath=${encodeURIComponent(filePath)}`} download>
|
||||
<Button text={t("backup.download")} />
|
||||
<ActionButton icon="bx bx-download" text={t("backup.download")} />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -8,48 +8,73 @@ import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import mime_types from "../../../services/mime_types";
|
||||
import Column from "../../react/Column";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormSelect from "../../react/FormSelect";
|
||||
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { useStaticTooltip, useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||
import { CODE_THEME_DEFAULT_PREFIX as DEFAULT_PREFIX } from "../constants";
|
||||
import AutoReadOnlySize from "./components/AutoReadOnlySize";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import codeNoteSample from "./samples/code_note.txt?raw";
|
||||
|
||||
const SAMPLE_MIME = "application/typescript";
|
||||
|
||||
export default function CodeNoteSettings() {
|
||||
const [codeLineWrapEnabled, setCodeLineWrapEnabled] = useTriliumOptionBool("codeLineWrapEnabled");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor />
|
||||
<Appearance />
|
||||
<Editor wordWrapping={codeLineWrapEnabled} setWordWrapping={setCodeLineWrapEnabled} />
|
||||
<Appearance wordWrapping={codeLineWrapEnabled} />
|
||||
<CodeMimeTypes />
|
||||
<AutoReadOnlySize option="autoReadonlySizeCode" label={t("code_auto_read_only_size.label")} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Editor() {
|
||||
const [ vimKeymapEnabled, setVimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
|
||||
interface EditorProps {
|
||||
wordWrapping: boolean;
|
||||
setWordWrapping: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function Editor({ wordWrapping, setWordWrapping }: EditorProps) {
|
||||
const [vimKeymapEnabled, setVimKeymapEnabled] = useTriliumOptionBool("vimKeymapEnabled");
|
||||
const [autoReadonlySize, setAutoReadonlySize] = useTriliumOption("autoReadonlySizeCode");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("code-editor-options.title")}>
|
||||
<FormGroup name="vim-keymap-enabled" description={t("vim_key_bindings.enable_vim_keybindings")}>
|
||||
<FormCheckbox
|
||||
label={t("vim_key_bindings.use_vim_keybindings_in_code_notes")}
|
||||
currentValue={vimKeymapEnabled} onChange={setVimKeymapEnabled}
|
||||
<OptionsRowWithToggle
|
||||
name="word-wrap"
|
||||
label={t("code_theme.word_wrapping")}
|
||||
currentValue={wordWrapping}
|
||||
onChange={setWordWrapping}
|
||||
/>
|
||||
|
||||
<OptionsRow name="source-readonly-threshold" label={t("code_auto_read_only_size.label")} description={t("text_auto_read_only_size.description")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min={0}
|
||||
unit={t("text_auto_read_only_size.unit")}
|
||||
currentValue={autoReadonlySize}
|
||||
onBlur={setAutoReadonlySize}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRowWithToggle
|
||||
name="vim-keymap-enabled"
|
||||
label={t("vim_key_bindings.use_vim_keybindings_in_code_notes")}
|
||||
description={t("vim_key_bindings.enable_vim_keybindings")}
|
||||
currentValue={vimKeymapEnabled}
|
||||
onChange={setVimKeymapEnabled}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function Appearance() {
|
||||
const [ codeNoteTheme, setCodeNoteTheme ] = useTriliumOption("codeNoteTheme");
|
||||
const [ codeLineWrapEnabled, setCodeLineWrapEnabled ] = useTriliumOptionBool("codeLineWrapEnabled");
|
||||
interface AppearanceProps {
|
||||
wordWrapping: boolean;
|
||||
}
|
||||
|
||||
function Appearance({ wordWrapping }: AppearanceProps) {
|
||||
const [codeNoteTheme, setCodeNoteTheme] = useTriliumOption("codeNoteTheme");
|
||||
|
||||
const themes = useMemo(() => {
|
||||
return ColorThemes.map(({ id, name }) => ({
|
||||
@@ -60,25 +85,15 @@ function Appearance() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("code_theme.title")}>
|
||||
<div className="row" style={{ marginBottom: "15px" }}>
|
||||
<FormGroup name="color-scheme" label={t("code_theme.color-scheme")} className="col-md-6" style={{ marginBottom: 0 }}>
|
||||
<FormSelect
|
||||
values={themes}
|
||||
keyProperty="id" titleProperty="name"
|
||||
currentValue={codeNoteTheme} onChange={setCodeNoteTheme}
|
||||
/>
|
||||
</FormGroup>
|
||||
<OptionsRow name="color-scheme" label={t("code_theme.color-scheme")}>
|
||||
<FormSelect
|
||||
values={themes}
|
||||
keyProperty="id" titleProperty="name"
|
||||
currentValue={codeNoteTheme} onChange={setCodeNoteTheme}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<Column className="side-checkbox">
|
||||
<FormCheckbox
|
||||
name="word-wrap"
|
||||
label={t("code_theme.word_wrapping")}
|
||||
currentValue={codeLineWrapEnabled} onChange={setCodeLineWrapEnabled}
|
||||
/>
|
||||
</Column>
|
||||
</div>
|
||||
|
||||
<CodeNotePreview wordWrapping={codeLineWrapEnabled} themeName={codeNoteTheme} />
|
||||
<CodeNotePreview wordWrapping={wordWrapping} themeName={codeNoteTheme} />
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.option-row:last-of-type {
|
||||
.option-row:last-child {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.option-row-link.use-tn-links {
|
||||
.option-row-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
margin-inline: calc(-1 * var(--options-card-padding, 15px));
|
||||
@@ -67,3 +67,37 @@
|
||||
.option-row-link:hover {
|
||||
background: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
button.option-row-link {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
width: calc(100% + 2 * var(--options-card-padding, 15px));
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.option-row-link:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-engine-templates {
|
||||
--badge-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-engine-templates .ext-badge {
|
||||
--color: var(--input-background-color);
|
||||
color: var(--main-text-color);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.search-engine-templates .ext-badge.selected {
|
||||
--color: var(--accented-background-color);
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import "./OptionsRow.css";
|
||||
|
||||
import { cloneElement, VNode } from "preact";
|
||||
import { cloneElement, ComponentChildren, VNode } from "preact";
|
||||
|
||||
import FormToggle from "../../../react/FormToggle";
|
||||
import { useUniqueName } from "../../../react/hooks";
|
||||
|
||||
interface OptionsRowProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
label?: ComponentChildren;
|
||||
description?: ComponentChildren;
|
||||
children: VNode;
|
||||
centered?: boolean;
|
||||
/** When true, stacks label above input with full-width input */
|
||||
@@ -16,7 +17,7 @@ interface OptionsRowProps {
|
||||
|
||||
export default function OptionsRow({ name, label, description, children, centered, stacked }: OptionsRowProps) {
|
||||
const id = useUniqueName(name);
|
||||
const childWithId = cloneElement(children, { id });
|
||||
const childWithId = cloneElement(children, { id, name: (children.props as { name?: string }).name ?? name });
|
||||
|
||||
const className = `option-row ${centered ? "centered" : ""} ${stacked ? "stacked" : ""}`;
|
||||
|
||||
@@ -41,7 +42,7 @@ interface OptionsRowLinkProps {
|
||||
|
||||
export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps) {
|
||||
return (
|
||||
<a href={href} className="option-row option-row-link use-tn-links no-tooltip-preview">
|
||||
<a href={href} className="option-row option-row-link no-tooltip-preview">
|
||||
<div className="option-row-label">
|
||||
<label style={{ cursor: "pointer" }}>{label}</label>
|
||||
{description && <small className="option-row-description">{description}</small>}
|
||||
@@ -52,3 +53,56 @@ export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
interface OptionsRowWithToggleProps {
|
||||
name: string;
|
||||
label: ComponentChildren;
|
||||
description?: ComponentChildren;
|
||||
currentValue: boolean | null;
|
||||
onChange: (newValue: boolean) => void;
|
||||
disabled?: boolean;
|
||||
helpPage?: string;
|
||||
}
|
||||
|
||||
export function OptionsRowWithToggle({ name, label, description, currentValue, onChange, disabled, helpPage }: OptionsRowWithToggleProps) {
|
||||
return (
|
||||
<OptionsRow name={name} label={label} description={description}>
|
||||
<FormToggle
|
||||
switchOnName=""
|
||||
switchOffName=""
|
||||
currentValue={currentValue}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
helpPage={helpPage}
|
||||
/>
|
||||
</OptionsRow>
|
||||
);
|
||||
}
|
||||
|
||||
interface OptionsRowWithButtonProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function OptionsRowWithButton({ label, description, icon, onClick }: OptionsRowWithButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="option-row option-row-link"
|
||||
onClick={onClick}
|
||||
aria-label={label}
|
||||
>
|
||||
<div className="option-row-label">
|
||||
<span style={{ cursor: "pointer" }}>{label}</span>
|
||||
{description && <small className="option-row-description">{description}</small>}
|
||||
</div>
|
||||
{icon && (
|
||||
<div className="option-row-input">
|
||||
<span className={icon} />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,47 @@
|
||||
import type { ComponentChildren } from "preact";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
import HelpButton from "../../../react/HelpButton";
|
||||
|
||||
interface OptionsSectionProps {
|
||||
title?: ComponentChildren;
|
||||
description?: ComponentChildren;
|
||||
children: ComponentChildren;
|
||||
noCard?: boolean;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
helpUrl?: string;
|
||||
}
|
||||
|
||||
export default function OptionsSection({ title, children, noCard, className, ...rest }: OptionsSectionProps) {
|
||||
return (
|
||||
<div className={`options-section ${noCard ? "tn-no-card" : ""} ${className ?? ""}`} {...rest}>
|
||||
export default function OptionsSection({ title, description, children, noCard, className, helpUrl, ...rest }: OptionsSectionProps) {
|
||||
const header = (title || helpUrl) && (
|
||||
<div className="options-section-header">
|
||||
{title && <h4>{title}</h4>}
|
||||
{helpUrl && <HelpButton helpPage={helpUrl} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{description && <p className="options-section-description">{description}</p>}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
if (noCard) {
|
||||
return (
|
||||
<div className={`options-section tn-no-card ${className ?? ""}`} {...rest}>
|
||||
{header}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`options-section ${className ?? ""}`} {...rest}>
|
||||
{header}
|
||||
<div className="options-section-card">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.options-section .radio-with-illustration {
|
||||
.options-section-card .radio-with-illustration {
|
||||
list-style-type: none;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -6,10 +6,7 @@ import OptionsSection from "./components/OptionsSection";
|
||||
import { useTriliumOption, useTriliumOptionJson } from "../../react/hooks";
|
||||
import type { Locale } from "@triliumnext/commons";
|
||||
import { isElectron, restartDesktopApp } from "../../../services/utils";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import FormText from "../../react/FormText";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import Button from "../../react/Button";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import RelatedSettings from "./components/RelatedSettings";
|
||||
@@ -96,10 +93,13 @@ function DateSettings() {
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow name="first-week-of-year" label={t("i18n.first-week-of-the-year")}>
|
||||
<FormRadioGroup
|
||||
<OptionsRow name="first-week-of-year" label={t("i18n.first-week-of-the-year")} description={t("i18n.first-week-warning")}>
|
||||
<FormSelect
|
||||
name="first-week-of-year"
|
||||
currentValue={firstWeekOfYear} onChange={setFirstWeekOfYear}
|
||||
currentValue={firstWeekOfYear}
|
||||
onChange={setFirstWeekOfYear}
|
||||
keyProperty="value"
|
||||
titleProperty="label"
|
||||
values={[
|
||||
{ value: "0", label: t("i18n.first-week-contains-first-day") },
|
||||
{ value: "1", label: t("i18n.first-week-contains-first-thursday") },
|
||||
@@ -117,14 +117,6 @@ function DateSettings() {
|
||||
(_, i) => ({ days: String(i + 1) }))} />
|
||||
</OptionsRow>}
|
||||
|
||||
<FormText>
|
||||
<RawHtml html={t("i18n.first-week-info")} />
|
||||
</FormText>
|
||||
|
||||
<Admonition type="warning">
|
||||
{t("i18n.first-week-warning")}
|
||||
</Admonition>
|
||||
|
||||
<OptionsRow name="restart" centered>
|
||||
<Button
|
||||
name="restart-app-button"
|
||||
|
||||
@@ -5,9 +5,8 @@ import { isExperimentalFeatureEnabled } from "../../../services/experimental_fea
|
||||
import { t } from "../../../services/i18n";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import Button from "../../react/Button";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import AddProviderModal, { type LlmProviderConfig, PROVIDER_TYPES } from "./llm/AddProviderModal";
|
||||
|
||||
@@ -92,13 +91,13 @@ function McpSettings() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("llm.mcp_title")}>
|
||||
<OptionsRow name="mcp-enabled" label={t("llm.mcp_enabled")} description={t("llm.mcp_enabled_description")}>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={mcpEnabled}
|
||||
onChange={setMcpEnabled}
|
||||
/>
|
||||
</OptionsRow>
|
||||
<OptionsRowWithToggle
|
||||
name="mcp-enabled"
|
||||
label={t("llm.mcp_enabled")}
|
||||
description={t("llm.mcp_enabled_description")}
|
||||
currentValue={mcpEnabled}
|
||||
onChange={setMcpEnabled}
|
||||
/>
|
||||
|
||||
{mcpEnabled && (
|
||||
<OptionsRow name="mcp-endpoint" label={t("llm.mcp_endpoint_title")} description={t("llm.mcp_endpoint_description")}>
|
||||
|
||||
@@ -5,10 +5,9 @@ import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { isElectron } from "../../../services/utils";
|
||||
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import Slider from "../../react/Slider";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import RelatedSettings from "./components/RelatedSettings";
|
||||
|
||||
@@ -29,21 +28,21 @@ function ImageSettings() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("images.images_section_title")}>
|
||||
<OptionsRow name="download-images-automatically" label={t("images.download_images_automatically")} description={t("images.download_images_description")}>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={downloadImagesAutomatically}
|
||||
onChange={setDownloadImagesAutomatically}
|
||||
/>
|
||||
</OptionsRow>
|
||||
<OptionsRowWithToggle
|
||||
name="download-images-automatically"
|
||||
label={t("images.download_images_automatically")}
|
||||
description={t("images.download_images_description")}
|
||||
currentValue={downloadImagesAutomatically}
|
||||
onChange={setDownloadImagesAutomatically}
|
||||
/>
|
||||
|
||||
<OptionsRow name="image-compression-enabled" label={t("images.enable_image_compression")} description={t("images.enable_image_compression_description")}>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={compressImages}
|
||||
onChange={setCompressImages}
|
||||
/>
|
||||
</OptionsRow>
|
||||
<OptionsRowWithToggle
|
||||
name="image-compression-enabled"
|
||||
label={t("images.enable_image_compression")}
|
||||
description={t("images.enable_image_compression_description")}
|
||||
currentValue={compressImages}
|
||||
onChange={setCompressImages}
|
||||
/>
|
||||
|
||||
<OptionsRow name="image-max-width-height" label={t("images.max_image_dimensions")} description={t("images.max_image_dimensions_description")}>
|
||||
<FormTextBoxWithUnit
|
||||
@@ -72,13 +71,13 @@ function OcrSettings() {
|
||||
return (
|
||||
<>
|
||||
<OptionsSection title={t("images.ocr_section_title")}>
|
||||
<OptionsRow name="ocr-auto-process" label={t("images.ocr_auto_process")} description={t("images.ocr_auto_process_description")}>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={ocrAutoProcess}
|
||||
onChange={setOcrAutoProcess}
|
||||
/>
|
||||
</OptionsRow>
|
||||
<OptionsRowWithToggle
|
||||
name="ocr-auto-process"
|
||||
label={t("images.ocr_auto_process")}
|
||||
description={t("images.ocr_auto_process_description")}
|
||||
currentValue={ocrAutoProcess}
|
||||
onChange={setOcrAutoProcess}
|
||||
/>
|
||||
|
||||
<OptionsRow name="ocr-min-confidence" label={`${t("images.ocr_min_confidence")} (${Math.round(parseFloat(ocrMinConfidence ?? "0.75") * 100)}%)`} description={t("images.ocr_confidence_description")}>
|
||||
<Slider
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import type React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import search from "../../../services/search";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { isElectron } from "../../../services/utils";
|
||||
import { Badge } from "../../react/Badge";
|
||||
import Button from "../../react/Button";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormSelect from "../../react/FormSelect";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsRow, { OptionsRowWithButton, OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import TimeSelector from "./components/TimeSelector";
|
||||
|
||||
@@ -30,8 +25,7 @@ export default function OtherSettings() {
|
||||
</>}
|
||||
<NoteErasureTimeout />
|
||||
<AttachmentErasureTimeout />
|
||||
<RevisionSnapshotInterval />
|
||||
<RevisionSnapshotLimit />
|
||||
<RevisionSettings />
|
||||
<HtmlImportTags />
|
||||
<ShareSettings />
|
||||
<NetworkSettings />
|
||||
@@ -45,29 +39,21 @@ function SearchSettings() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("search.title")}>
|
||||
<OptionsRow
|
||||
<OptionsRowWithToggle
|
||||
name="search-fuzzy-matching"
|
||||
label={t("search.fuzzy_matching_label")}
|
||||
description={t("search.fuzzy_matching_description")}
|
||||
>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={fuzzyEnabled}
|
||||
onChange={setFuzzyEnabled}
|
||||
/>
|
||||
</OptionsRow>
|
||||
currentValue={fuzzyEnabled}
|
||||
onChange={setFuzzyEnabled}
|
||||
/>
|
||||
|
||||
<OptionsRow
|
||||
<OptionsRowWithToggle
|
||||
name="search-autocomplete-fuzzy"
|
||||
label={t("search.autocomplete_fuzzy_label")}
|
||||
description={t("search.autocomplete_fuzzy_description")}
|
||||
>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={autocompleteFuzzy}
|
||||
onChange={setAutocompleteFuzzy}
|
||||
/>
|
||||
</OptionsRow>
|
||||
currentValue={autocompleteFuzzy}
|
||||
onChange={setAutocompleteFuzzy}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
@@ -78,47 +64,45 @@ function SearchEngineSettings() {
|
||||
|
||||
const searchEngines = useMemo(() => {
|
||||
return [
|
||||
{ url: "https://www.bing.com/search?q={keyword}", name: t("search_engine.bing") },
|
||||
{ url: "https://www.baidu.com/s?wd={keyword}", name: t("search_engine.baidu") },
|
||||
{ url: "https://duckduckgo.com/?q={keyword}", name: t("search_engine.duckduckgo") },
|
||||
{ url: "https://www.google.com/search?q={keyword}", name: t("search_engine.google") }
|
||||
{ url: "https://www.bing.com/search?q={keyword}", name: t("search_engine.bing"), icon: "bx bxl-bing" },
|
||||
{ url: "https://www.baidu.com/s?wd={keyword}", name: t("search_engine.baidu"), icon: "bx bxl-baidu" },
|
||||
{ url: "https://www.google.com/search?q={keyword}", name: t("search_engine.google"), icon: "bx bxl-google" }
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("search_engine.title")}>
|
||||
<FormText>{t("search_engine.custom_search_engine_info")}</FormText>
|
||||
<OptionsSection title={t("search_engine.title")} description={t("search_engine.custom_search_engine_info")}>
|
||||
<OptionsRow name="predefined-templates" label={t("search_engine.predefined_templates_label")}>
|
||||
<div className="search-engine-templates">
|
||||
{searchEngines.map(engine => (
|
||||
<Badge
|
||||
key={engine.url}
|
||||
icon={engine.icon}
|
||||
text={engine.name}
|
||||
className={customSearchEngineUrl === engine.url ? "selected" : ""}
|
||||
onClick={() => {
|
||||
setCustomSearchEngineName(engine.name);
|
||||
setCustomSearchEngineUrl(engine.url);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</OptionsRow>
|
||||
|
||||
<FormGroup name="predefined-search-engine" label={t("search_engine.predefined_templates_label")}>
|
||||
<FormSelect
|
||||
values={searchEngines}
|
||||
currentValue={customSearchEngineUrl}
|
||||
keyProperty="url" titleProperty="name"
|
||||
onChange={newValue => {
|
||||
const searchEngine = searchEngines.find(e => e.url === newValue);
|
||||
if (!searchEngine) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCustomSearchEngineName(searchEngine.name);
|
||||
setCustomSearchEngineUrl(searchEngine.url);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="custom-name" label={t("search_engine.custom_name_label")}>
|
||||
<OptionsRow name="custom-name" label={t("search_engine.custom_name_label")}>
|
||||
<FormTextBox
|
||||
currentValue={customSearchEngineName} onChange={setCustomSearchEngineName}
|
||||
currentValue={customSearchEngineName} onBlur={setCustomSearchEngineName}
|
||||
placeholder={t("search_engine.custom_name_placeholder")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsRow>
|
||||
|
||||
<FormGroup name="custom-url" label={t("search_engine.custom_url_label")}>
|
||||
<OptionsRow name="custom-url" label={t("search_engine.custom_url_label")} description={t("search_engine.custom_url_description")} stacked>
|
||||
<FormTextBox
|
||||
currentValue={customSearchEngineUrl} onChange={setCustomSearchEngineUrl}
|
||||
currentValue={customSearchEngineUrl} onBlur={setCustomSearchEngineUrl}
|
||||
placeholder={t("search_engine.custom_url_placeholder")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
@@ -128,9 +112,10 @@ function TrayOptionsSettings() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("tray.title")}>
|
||||
<FormCheckbox
|
||||
<OptionsRowWithToggle
|
||||
name="tray-enabled"
|
||||
label={t("tray.enable_tray")}
|
||||
description={t("tray.enable_tray_description")}
|
||||
currentValue={!disableTray}
|
||||
onChange={trayEnabled => setDisableTray(!trayEnabled)}
|
||||
/>
|
||||
@@ -141,17 +126,18 @@ function TrayOptionsSettings() {
|
||||
function NoteErasureTimeout() {
|
||||
return (
|
||||
<OptionsSection title={t("note_erasure_timeout.note_erasure_timeout_title")}>
|
||||
<FormText>{t("note_erasure_timeout.note_erasure_description")}</FormText>
|
||||
<FormGroup name="erase-entities-after" label={t("note_erasure_timeout.erase_notes_after")}>
|
||||
<FormText>{t("note_erasure_timeout.description")}</FormText>
|
||||
|
||||
<OptionsRow name="erase-entities-after" label={t("note_erasure_timeout.erase_notes_after")} description={t("note_erasure_timeout.erase_notes_after_description")}>
|
||||
<TimeSelector
|
||||
name="erase-entities-after"
|
||||
optionValueId="eraseEntitiesAfterTimeInSeconds" optionTimeScaleId="eraseEntitiesAfterTimeScale"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormText>{t("note_erasure_timeout.manual_erasing_description")}</FormText>
|
||||
</OptionsRow>
|
||||
|
||||
<Button
|
||||
text={t("note_erasure_timeout.erase_deleted_notes_now")}
|
||||
<OptionsRowWithButton
|
||||
label={t("note_erasure_timeout.erase_deleted_notes_now")}
|
||||
description={t("note_erasure_timeout.manual_erasing_description")}
|
||||
onClick={() => {
|
||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||
toast.showMessage(t("note_erasure_timeout.deleted_notes_erased"));
|
||||
@@ -165,17 +151,18 @@ function NoteErasureTimeout() {
|
||||
function AttachmentErasureTimeout() {
|
||||
return (
|
||||
<OptionsSection title={t("attachment_erasure_timeout.attachment_erasure_timeout")}>
|
||||
<FormText>{t("attachment_erasure_timeout.attachment_auto_deletion_description")}</FormText>
|
||||
<FormGroup name="erase-unused-attachments-after" label={t("attachment_erasure_timeout.erase_attachments_after")}>
|
||||
<FormText>{t("attachment_erasure_timeout.description")}</FormText>
|
||||
|
||||
<OptionsRow name="erase-unused-attachments-after" label={t("attachment_erasure_timeout.erase_attachments_after")} description={t("attachment_erasure_timeout.erase_attachments_after_description")}>
|
||||
<TimeSelector
|
||||
name="erase-unused-attachments-after"
|
||||
optionValueId="eraseUnusedAttachmentsAfterSeconds" optionTimeScaleId="eraseUnusedAttachmentsAfterTimeScale"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormText>{t("attachment_erasure_timeout.manual_erasing_description")}</FormText>
|
||||
</OptionsRow>
|
||||
|
||||
<Button
|
||||
text={t("attachment_erasure_timeout.erase_unused_attachments_now")}
|
||||
<OptionsRowWithButton
|
||||
label={t("attachment_erasure_timeout.erase_unused_attachments_now")}
|
||||
description={t("attachment_erasure_timeout.manual_erasing_description")}
|
||||
onClick={() => {
|
||||
server.post("notes/erase-unused-attachments-now").then(() => {
|
||||
toast.showMessage(t("attachment_erasure_timeout.unused_attachments_erased"));
|
||||
@@ -186,49 +173,36 @@ function AttachmentErasureTimeout() {
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionSnapshotInterval() {
|
||||
function RevisionSettings() {
|
||||
const [ revisionSnapshotNumberLimit, setRevisionSnapshotNumberLimit ] = useTriliumOption("revisionSnapshotNumberLimit");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("revisions_snapshot_interval.note_revisions_snapshot_interval_title")}>
|
||||
<FormText>
|
||||
<Trans
|
||||
i18nKey="revisions_snapshot_interval.note_revisions_snapshot_description"
|
||||
components={{ doc: <a href="https://triliumnext.github.io/Docs/Wiki/note-revisions.html" class="external" /> as React.ReactElement }}
|
||||
/>
|
||||
</FormText>
|
||||
<FormGroup name="revision-snapshot-time-interval" label={t("revisions_snapshot_interval.snapshot_time_interval_label")}>
|
||||
<OptionsSection title={t("revisions.title")}>
|
||||
<OptionsRow name="revision-snapshot-time-interval" label={t("revisions_snapshot_interval.snapshot_time_interval_label")} description={t("revisions_snapshot_interval.note_revisions_snapshot_description_short")}>
|
||||
<TimeSelector
|
||||
name="revision-snapshot-time-interval"
|
||||
optionValueId="revisionSnapshotTimeInterval" optionTimeScaleId="revisionSnapshotTimeIntervalTimeScale"
|
||||
minimumSeconds={10}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
</OptionsRow>
|
||||
|
||||
function RevisionSnapshotLimit() {
|
||||
const [ revisionSnapshotNumberLimit, setRevisionSnapshotNumberLimit ] = useTriliumOption("revisionSnapshotNumberLimit");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("revisions_snapshot_limit.note_revisions_snapshot_limit_title")}>
|
||||
<FormText>{t("revisions_snapshot_limit.note_revisions_snapshot_limit_description")}</FormText>
|
||||
|
||||
<FormGroup name="revision-snapshot-number-limit">
|
||||
<OptionsRow name="revision-snapshot-number-limit" label={t("revisions_snapshot_limit.snapshot_number_limit_label")} description={t("revisions_snapshot_limit.note_revisions_snapshot_limit_description_short")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min={-1}
|
||||
currentValue={revisionSnapshotNumberLimit}
|
||||
unit={t("revisions_snapshot_limit.snapshot_number_limit_unit")}
|
||||
onChange={value => {
|
||||
onBlur={value => {
|
||||
const newValue = parseInt(value, 10);
|
||||
if (!isNaN(newValue) && newValue >= -1) {
|
||||
setRevisionSnapshotNumberLimit(newValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsRow>
|
||||
|
||||
<Button
|
||||
text={t("revisions_snapshot_limit.erase_excess_revision_snapshots")}
|
||||
<OptionsRowWithButton
|
||||
label={t("revisions_snapshot_limit.erase_excess_revision_snapshots")}
|
||||
description={t("revisions_snapshot_limit.erase_excess_revision_snapshots_description")}
|
||||
onClick={async () => {
|
||||
await server.post("revisions/erase-all-excess-revisions");
|
||||
toast.showMessage(t("revisions_snapshot_limit.erase_excess_revision_snapshots_prompt"));
|
||||
@@ -283,34 +257,35 @@ function ShareSettings() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("share.title")}>
|
||||
<FormGroup name="redirectBareDomain" description={t("share.redirect_bare_domain_description")}>
|
||||
<FormCheckbox
|
||||
label={t(t("share.redirect_bare_domain"))}
|
||||
currentValue={redirectBareDomain}
|
||||
onChange={async value => {
|
||||
if (value) {
|
||||
const shareRootNotes = await search.searchForNotes("#shareRoot");
|
||||
const sharedShareRootNote = shareRootNotes.find((note) => note.isShared());
|
||||
<OptionsRowWithToggle
|
||||
name="redirect-bare-domain"
|
||||
label={t("share.redirect_bare_domain")}
|
||||
description={t("share.redirect_bare_domain_description")}
|
||||
currentValue={redirectBareDomain}
|
||||
onChange={async value => {
|
||||
if (value) {
|
||||
const shareRootNotes = await search.searchForNotes("#shareRoot");
|
||||
const sharedShareRootNote = shareRootNotes.find((note) => note.isShared());
|
||||
|
||||
if (sharedShareRootNote) {
|
||||
toast.showMessage(t("share.share_root_found", { noteTitle: sharedShareRootNote.title }));
|
||||
} else if (shareRootNotes.length > 0) {
|
||||
toast.showError(t("share.share_root_not_shared", { noteTitle: shareRootNotes[0].title }));
|
||||
} else {
|
||||
toast.showError(t("share.share_root_not_found"));
|
||||
}
|
||||
if (sharedShareRootNote) {
|
||||
toast.showMessage(t("share.share_root_found", { noteTitle: sharedShareRootNote.title }));
|
||||
} else if (shareRootNotes.length > 0) {
|
||||
toast.showError(t("share.share_root_not_shared", { noteTitle: shareRootNotes[0].title }));
|
||||
} else {
|
||||
toast.showError(t("share.share_root_not_found"));
|
||||
}
|
||||
setRedirectBareDomain(value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
setRedirectBareDomain(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormGroup name="showLoginInShareTheme" description={t("share.show_login_link_description")}>
|
||||
<FormCheckbox
|
||||
label={t("share.show_login_link")}
|
||||
currentValue={showLogInShareTheme} onChange={setShowLogInShareTheme}
|
||||
/>
|
||||
</FormGroup>
|
||||
<OptionsRowWithToggle
|
||||
name="show-login-in-share-theme"
|
||||
label={t("share.show_login_link")}
|
||||
description={t("share.show_login_link_description")}
|
||||
currentValue={showLogInShareTheme}
|
||||
onChange={setShowLogInShareTheme}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
@@ -320,10 +295,12 @@ function NetworkSettings() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("network_connections.network_connections_title")}>
|
||||
<FormCheckbox
|
||||
<OptionsRowWithToggle
|
||||
name="check-for-updates"
|
||||
label={t("network_connections.check_for_updates")}
|
||||
currentValue={checkForUpdates} onChange={setCheckForUpdates}
|
||||
description={t("network_connections.check_for_updates_description")}
|
||||
currentValue={checkForUpdates}
|
||||
onChange={setCheckForUpdates}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState } from "preact/hooks"
|
||||
import { t } from "../../../services/i18n"
|
||||
import server from "../../../services/server"
|
||||
import toast from "../../../services/toast"
|
||||
import Alert from "../../react/Alert"
|
||||
import Button from "../../react/Button"
|
||||
import FormGroup from "../../react/FormGroup"
|
||||
import FormTextBox from "../../react/FormTextBox"
|
||||
import LinkButton from "../../react/LinkButton"
|
||||
import OptionsSection from "./components/OptionsSection"
|
||||
import protected_session_holder from "../../../services/protected_session_holder"
|
||||
import { ChangePasswordResponse } from "@triliumnext/commons"
|
||||
import dialog from "../../../services/dialog"
|
||||
import TimeSelector from "./components/TimeSelector"
|
||||
import FormText from "../../react/FormText"
|
||||
import { useState } from "preact/hooks";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import Button from "../../react/Button";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import OptionsRow, { OptionsRowWithButton } from "./components/OptionsRow";
|
||||
import protected_session_holder from "../../../services/protected_session_holder";
|
||||
import { ChangePasswordResponse } from "@triliumnext/commons";
|
||||
import dialog from "../../../services/dialog";
|
||||
import TimeSelector from "./components/TimeSelector";
|
||||
import Modal from "../../react/Modal";
|
||||
|
||||
export default function PasswordSettings() {
|
||||
return (
|
||||
@@ -20,104 +20,142 @@ export default function PasswordSettings() {
|
||||
<ChangePassword />
|
||||
<ProtectedSessionTimeout />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ChangePassword() {
|
||||
const [ oldPassword, setOldPassword ] = useState("");
|
||||
const [ newPassword1, setNewPassword1 ] = useState("");
|
||||
const [ newPassword2, setNewPassword2 ] = useState("");
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("password.heading")}>
|
||||
<Alert type="warning">
|
||||
{t("password.alert_message")}
|
||||
|
||||
<LinkButton
|
||||
text={t("password.reset_link")}
|
||||
onClick={async () => {
|
||||
if (!confirm(t("password.reset_confirmation"))) {
|
||||
return;
|
||||
}
|
||||
<OptionsRowWithButton
|
||||
label={t("password.change_password")}
|
||||
description={t("password.change_password_description")}
|
||||
icon="bx bx-chevron-right"
|
||||
onClick={() => setShowModal(true)}
|
||||
/>
|
||||
|
||||
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
|
||||
toast.showError(t("password.reset_success_message"));
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
<OptionsRowWithButton
|
||||
label={t("password.reset_password")}
|
||||
description={t("password.reset_password_description")}
|
||||
icon="bx bx-chevron-right"
|
||||
onClick={async () => {
|
||||
if (!await dialog.confirm(t("password.reset_confirmation"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
<form onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
|
||||
toast.showError(t("password.reset_success_message"));
|
||||
}}
|
||||
/>
|
||||
|
||||
setOldPassword("");
|
||||
setNewPassword1("");
|
||||
setNewPassword2("");
|
||||
|
||||
if (newPassword1 !== newPassword2) {
|
||||
toast.showError(t("password.password_mismatch"));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await server
|
||||
.post<ChangePasswordResponse>("password/change", {
|
||||
current_password: oldPassword,
|
||||
new_password: newPassword1
|
||||
})
|
||||
if (result.success) {
|
||||
await dialog.info(t("password.password_changed_success"));
|
||||
|
||||
// password changed so current protected session is invalid and needs to be cleared
|
||||
protected_session_holder.resetProtectedSession();
|
||||
} else if (result.message) {
|
||||
toast.showError(result.message);
|
||||
}
|
||||
}}>
|
||||
<FormGroup name="old-password" label={t("password.old_password")}>
|
||||
<FormTextBox
|
||||
type="password"
|
||||
currentValue={oldPassword} onChange={setOldPassword}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="new-password1" label={t("password.new_password")}>
|
||||
<FormTextBox
|
||||
type="password"
|
||||
currentValue={newPassword1} onChange={setNewPassword1}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="new-password2" label={t("password.new_password_confirmation")}>
|
||||
<FormTextBox
|
||||
type="password"
|
||||
currentValue={newPassword2} onChange={setNewPassword2}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Button
|
||||
text={t("password.change_password")}
|
||||
kind="primary"
|
||||
/>
|
||||
</form>
|
||||
{createPortal(
|
||||
<ChangePasswordModal show={showModal} onHidden={() => setShowModal(false)} />,
|
||||
document.body
|
||||
)}
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
show: boolean;
|
||||
onHidden: () => void;
|
||||
}
|
||||
|
||||
function ChangePasswordModal({ show, onHidden }: ChangePasswordModalProps) {
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword1, setNewPassword1] = useState("");
|
||||
const [newPassword2, setNewPassword2] = useState("");
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (newPassword1 !== newPassword2) {
|
||||
toast.showError(t("password.password_mismatch"));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await server.post<ChangePasswordResponse>("password/change", {
|
||||
current_password: oldPassword,
|
||||
new_password: newPassword1
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
onHidden();
|
||||
setOldPassword("");
|
||||
setNewPassword1("");
|
||||
setNewPassword2("");
|
||||
await dialog.info(t("password.password_changed_success"));
|
||||
|
||||
// password changed so current protected session is invalid and needs to be cleared
|
||||
protected_session_holder.resetProtectedSession();
|
||||
} else if (result.message) {
|
||||
toast.showError(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHidden = () => {
|
||||
setOldPassword("");
|
||||
setNewPassword1("");
|
||||
setNewPassword2("");
|
||||
onHidden();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
onHidden={handleHidden}
|
||||
onSubmit={handleSubmit}
|
||||
title={t("password.change_password_heading")}
|
||||
className="change-password-modal"
|
||||
size="md"
|
||||
footer={
|
||||
<>
|
||||
<Button text={t("password.cancel")} onClick={handleHidden} />
|
||||
<Button text={t("password.change_password")} kind="primary" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormGroup name="old-password" label={t("password.old_password")}>
|
||||
<FormTextBox
|
||||
type="password"
|
||||
currentValue={oldPassword}
|
||||
onChange={setOldPassword}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="new-password1" label={t("password.new_password")}>
|
||||
<FormTextBox
|
||||
type="password"
|
||||
currentValue={newPassword1}
|
||||
onChange={setNewPassword1}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="new-password2" label={t("password.new_password_confirmation")}>
|
||||
<FormTextBox
|
||||
type="password"
|
||||
currentValue={newPassword2}
|
||||
onChange={setNewPassword2}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function ProtectedSessionTimeout() {
|
||||
return (
|
||||
<OptionsSection title={t("password.protected_session_timeout")}>
|
||||
<FormText>
|
||||
{t("password.protected_session_timeout_description")}
|
||||
|
||||
<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/protected-notes.html" className="external">{t("password.wiki")}</a> {t("password.for_more_info")}
|
||||
</FormText>
|
||||
|
||||
<FormGroup name="protected-session-timeout" label={t("password.protected_session_timeout_label")}>
|
||||
<TimeSelector
|
||||
<OptionsRow
|
||||
name="protected-session-timeout"
|
||||
label={t("password.protected_session_timeout_label")}
|
||||
description={<>{t("password.protected_session_timeout_description")} <a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/protected-notes.html">{t("password.wiki")}</a> {t("password.for_more_info")}</>}
|
||||
>
|
||||
<TimeSelector
|
||||
name="protected-session-timeout"
|
||||
optionValueId="protectedSessionTimeout" optionTimeScaleId="protectedSessionTimeoutTimeScale"
|
||||
optionValueId="protectedSessionTimeout"
|
||||
optionTimeScaleId="protectedSessionTimeoutTimeScale"
|
||||
minimumSeconds={60}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ import { t } from "../../../services/i18n";
|
||||
import { dynamicRequire, isElectron, restartDesktopApp } from "../../../services/utils";
|
||||
import Button from "../../react/Button";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import NoItems from "../../react/NoItems";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
|
||||
export default function SpellcheckSettings() {
|
||||
@@ -32,13 +31,12 @@ function ElectronSpellcheckSettings() {
|
||||
<OptionsSection title={t("spellcheck.title")}>
|
||||
<FormText>{t("spellcheck.restart-required")}</FormText>
|
||||
|
||||
<OptionsRow name="spell-check-enabled" label={t("spellcheck.enable")}>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={spellCheckEnabled}
|
||||
onChange={setSpellCheckEnabled}
|
||||
/>
|
||||
</OptionsRow>
|
||||
<OptionsRowWithToggle
|
||||
name="spell-check-enabled"
|
||||
label={t("spellcheck.enable")}
|
||||
currentValue={spellCheckEnabled}
|
||||
onChange={setSpellCheckEnabled}
|
||||
/>
|
||||
|
||||
<OptionsRow name="restart" centered>
|
||||
<Button
|
||||
|
||||
@@ -1,69 +1,47 @@
|
||||
import { SyncTestResponse } from "@triliumnext/commons";
|
||||
import { useRef } from "preact/hooks";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { openInAppHelpFromUrl } from "../../../services/utils";
|
||||
import Button from "../../react/Button";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import { useTriliumOptions } from "../../react/hooks";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import { useTriliumOption } from "../../react/hooks";
|
||||
import OptionsRow, { OptionsRowWithButton } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import TimeSelector from "./components/TimeSelector";
|
||||
|
||||
export default function SyncOptions() {
|
||||
return (
|
||||
<>
|
||||
<SyncConfiguration />
|
||||
<SyncTest />
|
||||
</>
|
||||
);
|
||||
return <SyncConfiguration />;
|
||||
}
|
||||
|
||||
export function SyncConfiguration() {
|
||||
const [ options, setOptions ] = useTriliumOptions("syncServerHost", "syncProxy");
|
||||
const syncServerHost = useRef(options.syncServerHost);
|
||||
const syncProxy = useRef(options.syncProxy);
|
||||
const [syncServerHost, setSyncServerHost] = useTriliumOption("syncServerHost");
|
||||
const [syncProxy, setSyncProxy] = useTriliumOption("syncProxy");
|
||||
const [localHost, setLocalHost] = useState(syncServerHost);
|
||||
const [localProxy, setLocalProxy] = useState(syncProxy);
|
||||
|
||||
useEffect(() => setLocalHost(syncServerHost), [syncServerHost]);
|
||||
useEffect(() => setLocalProxy(syncProxy), [syncProxy]);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("sync_2.config_title")}>
|
||||
<form onSubmit={(e) => {
|
||||
setOptions({
|
||||
syncServerHost: syncServerHost.current,
|
||||
syncProxy: syncProxy.current
|
||||
});
|
||||
e.preventDefault();
|
||||
}}>
|
||||
<FormGroup name="sync-server-host" label={t("sync_2.server_address")}>
|
||||
<FormTextBox
|
||||
placeholder="https://<host>:<port>"
|
||||
currentValue={syncServerHost.current} onChange={(newValue) => syncServerHost.current = newValue}
|
||||
/>
|
||||
</FormGroup>
|
||||
<OptionsSection helpUrl="cbkrhQjrkKrh">
|
||||
<OptionsRow name="sync-server-host" label={t("sync_2.server_address")} description={t("sync_2.server_address_description")} stacked>
|
||||
<FormTextBox
|
||||
placeholder="https://<host>:<port>"
|
||||
currentValue={localHost}
|
||||
onChange={setLocalHost}
|
||||
onBlur={setSyncServerHost}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<FormGroup name="sync-proxy" label={t("sync_2.proxy_label")}
|
||||
description={<>
|
||||
<strong>{t("sync_2.note")}:</strong> {t("sync_2.note_description")}<br/>
|
||||
<RawHtml html={t("sync_2.special_value_description")} />
|
||||
</>}
|
||||
>
|
||||
<FormTextBox
|
||||
placeholder="https://<host>:<port>"
|
||||
currentValue={syncProxy.current} onChange={(newValue) => syncProxy.current = newValue}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "spaceBetween"}}>
|
||||
<Button text={t("sync_2.save")} kind="primary" />
|
||||
<Button text={t("sync_2.help")} onClick={() => openInAppHelpFromUrl("cbkrhQjrkKrh")} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
<OptionsRow name="sync-proxy" label={t("sync_2.proxy_label")} description={t("sync_2.proxy_description")} stacked>
|
||||
<FormTextBox
|
||||
placeholder="https://<host>:<port>"
|
||||
currentValue={localProxy}
|
||||
onChange={setLocalProxy}
|
||||
onBlur={setSyncProxy}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow name="sync-server-timeout" label={t("sync_2.timeout")} description={t("sync_2.timeout_description")}>
|
||||
<TimeSelector
|
||||
@@ -73,17 +51,15 @@ export function SyncConfiguration() {
|
||||
minimumSeconds={1}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
export function SyncTest() {
|
||||
return (
|
||||
<OptionsSection title={t("sync_2.test_title")}>
|
||||
<FormText>{t("sync_2.test_description")}</FormText>
|
||||
<Button
|
||||
text={t("sync_2.test_button")}
|
||||
<OptionsRowWithButton
|
||||
label={t("sync_2.test_button")}
|
||||
description={t("sync_2.test_description")}
|
||||
onClick={async () => {
|
||||
await Promise.all([
|
||||
setSyncServerHost(localHost),
|
||||
setSyncProxy(localProxy)
|
||||
]);
|
||||
const result = await server.post<SyncTestResponse>("sync/test");
|
||||
|
||||
if (result.success && result.message) {
|
||||
|
||||
125
apps/client/src/widgets/type_widgets/options/text_notes.css
Normal file
125
apps/client/src/widgets/type_widgets/options/text_notes.css
Normal file
@@ -0,0 +1,125 @@
|
||||
/* HeadingStyleSelector */
|
||||
.heading-style-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.heading-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
min-width: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.heading-preview-markdown .heading-prefix {
|
||||
color: var(--muted-text-color);
|
||||
font-weight: normal;
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
|
||||
.heading-preview-underline {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.heading-preview-underline .heading-underline {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: currentColor;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.heading-style-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Toolbar type illustration */
|
||||
.toolbar-illustration {
|
||||
width: 140px;
|
||||
height: 100px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--main-background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.toolbar-bar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
background: var(--left-pane-background-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--muted-text-color);
|
||||
border-radius: 2px;
|
||||
opacity: 0.5;
|
||||
|
||||
&.wide {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.document-area {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.text-line {
|
||||
height: 6px;
|
||||
background: var(--muted-text-color);
|
||||
border-radius: 2px;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.text-line-with-selection {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
height: 6px;
|
||||
|
||||
.text-segment {
|
||||
background: var(--muted-text-color);
|
||||
border-radius: 2px;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.text-selection {
|
||||
width: 30%;
|
||||
background: var(--accented-background-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-toolbar {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
padding: 4px 6px;
|
||||
background: var(--tooltip-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.toolbar-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--main-background-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
import { normalizeMimeTypeForCKEditor, type OptionNames } from "@triliumnext/commons";
|
||||
import "./text_notes.css";
|
||||
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import { Themes } from "@triliumnext/highlightjs";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import type React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { ensureMimeTypesForHighlighting, loadHighlightingTheme } from "../../../services/syntax_highlight";
|
||||
import { formatDateTime, toggleBodyClass } from "../../../services/utils";
|
||||
import Column from "../../react/Column";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import Dropdown from "../../react/Dropdown";
|
||||
import { FormListItem } from "../../react/FormList";
|
||||
import { FormSelectGroup, FormSelectWithGroups } from "../../react/FormSelect";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||
import KeyboardShortcut from "../../react/KeyboardShortcut";
|
||||
import { getHtml } from "../../react/RawHtml";
|
||||
import AutoReadOnlySize from "./components/AutoReadOnlySize";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import OptionsRow, { OptionsRowWithToggle } from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import RadioWithIllustration from "./components/RadioWithIllustration";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
@@ -30,12 +29,10 @@ export default function TextNoteSettings() {
|
||||
<>
|
||||
<FormattingToolbar />
|
||||
<EditorFeatures />
|
||||
<HeadingStyle />
|
||||
<Editor />
|
||||
<CodeBlockStyle />
|
||||
<TableOfContent />
|
||||
<HighlightsList />
|
||||
<AutoReadOnlySize option="autoReadonlySizeText" label={t("text_auto_read_only_size.label")} />
|
||||
<DateTimeFormatOptions />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -46,77 +43,188 @@ function FormattingToolbar() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("editing.editor_type.label")}>
|
||||
<FormRadioGroup
|
||||
name="editor-type"
|
||||
currentValue={textNoteEditorType} onChange={setTextNoteEditorType}
|
||||
values={[
|
||||
{
|
||||
value: "ckeditor-balloon",
|
||||
label: t("editing.editor_type.floating.title"),
|
||||
inlineDescription: t("editing.editor_type.floating.description")
|
||||
},
|
||||
{
|
||||
value: "ckeditor-classic",
|
||||
label: t("editing.editor_type.fixed.title"),
|
||||
inlineDescription: t("editing.editor_type.fixed.description")
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<OptionsRow name="editor-type" label={t("editing.editor_type.toolbar_style")}>
|
||||
<RadioWithIllustration
|
||||
currentValue={textNoteEditorType}
|
||||
onChange={setTextNoteEditorType}
|
||||
values={[
|
||||
{
|
||||
key: "ckeditor-balloon",
|
||||
text: t("editing.editor_type.floating.title"),
|
||||
illustration: <ToolbarIllustration type="floating" />
|
||||
},
|
||||
{
|
||||
key: "ckeditor-classic",
|
||||
text: t("editing.editor_type.fixed.title"),
|
||||
illustration: <ToolbarIllustration type="fixed" />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<FormCheckbox
|
||||
<OptionsRowWithToggle
|
||||
name="multiline-toolbar"
|
||||
label={t("editing.editor_type.multiline-toolbar")}
|
||||
currentValue={textNoteEditorMultilineToolbar} onChange={setTextNoteEditorMultilineToolbar}
|
||||
containerStyle={{ marginInlineStart: "1em" }}
|
||||
currentValue={textNoteEditorMultilineToolbar}
|
||||
onChange={setTextNoteEditorMultilineToolbar}
|
||||
disabled={textNoteEditorType === "ckeditor-balloon"}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarIllustration({ type }: { type: "floating" | "fixed" }) {
|
||||
return (
|
||||
<div className="toolbar-illustration">
|
||||
{type === "fixed" && (
|
||||
<div className="toolbar-bar">
|
||||
<ToolbarIcon />
|
||||
<ToolbarIcon />
|
||||
<ToolbarIcon />
|
||||
<ToolbarIcon wide />
|
||||
<ToolbarIcon />
|
||||
<ToolbarIcon />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="document-area">
|
||||
<div className="text-line" style={{ width: "90%" }} />
|
||||
<div className="text-line" style={{ width: "75%" }} />
|
||||
<div className="text-line-with-selection">
|
||||
<span className="text-segment" style={{ width: "20%" }} />
|
||||
<span className="text-selection" />
|
||||
<span className="text-segment" style={{ width: "35%" }} />
|
||||
</div>
|
||||
<div className="text-line" style={{ width: "85%" }} />
|
||||
<div className="text-line" style={{ width: "60%" }} />
|
||||
</div>
|
||||
|
||||
{type === "floating" && (
|
||||
<div className="floating-toolbar">
|
||||
<ToolbarIcon />
|
||||
<ToolbarIcon />
|
||||
<ToolbarIcon />
|
||||
<ToolbarIcon />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarIcon({ wide }: { wide?: boolean }) {
|
||||
return <div className={`toolbar-icon${wide ? " wide" : ""}`} />;
|
||||
}
|
||||
|
||||
function EditorFeatures() {
|
||||
const [emojiCompletionEnabled, setEmojiCompletionEnabled] = useTriliumOptionBool("textNoteEmojiCompletionEnabled");
|
||||
const [noteCompletionEnabled, setNoteCompletionEnabled] = useTriliumOptionBool("textNoteCompletionEnabled");
|
||||
const [slashCommandsEnabled, setSlashCommandsEnabled] = useTriliumOptionBool("textNoteSlashCommandsEnabled");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("editorfeatures.title")}>
|
||||
<EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
||||
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.note_completion_description")} />
|
||||
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.slash_commands_description")} />
|
||||
<OptionsRowWithToggle
|
||||
name="emoji-completion-enabled"
|
||||
label={t("editorfeatures.emoji_completion_enabled")}
|
||||
description={t("editorfeatures.emoji_completion_description")}
|
||||
currentValue={emojiCompletionEnabled}
|
||||
onChange={setEmojiCompletionEnabled}
|
||||
/>
|
||||
|
||||
<OptionsRowWithToggle
|
||||
name="note-completion-enabled"
|
||||
label={t("editorfeatures.note_completion_enabled")}
|
||||
description={t("editorfeatures.note_completion_description")}
|
||||
currentValue={noteCompletionEnabled}
|
||||
onChange={setNoteCompletionEnabled}
|
||||
/>
|
||||
|
||||
<OptionsRowWithToggle
|
||||
name="slash-commands-enabled"
|
||||
label={t("editorfeatures.slash_commands_enabled")}
|
||||
description={t("editorfeatures.slash_commands_description")}
|
||||
currentValue={slashCommandsEnabled}
|
||||
onChange={setSlashCommandsEnabled}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function EditorFeature({ optionName, name, label, description }: { optionName: OptionNames, name: string, label: string, description: string }) {
|
||||
const [ featureEnabled, setFeatureEnabled ] = useTriliumOptionBool(optionName);
|
||||
|
||||
return (
|
||||
<FormCheckbox
|
||||
name={name} label={label}
|
||||
currentValue={featureEnabled} onChange={setFeatureEnabled}
|
||||
hint={description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function HeadingStyle() {
|
||||
const [ headingStyle, setHeadingStyle ] = useTriliumOption("headingStyle");
|
||||
function Editor() {
|
||||
const [headingStyle, setHeadingStyle] = useTriliumOption("headingStyle");
|
||||
const [autoReadonlySize, setAutoReadonlySize] = useTriliumOption("autoReadonlySizeText");
|
||||
const [customDateTimeFormat, setCustomDateTimeFormat] = useTriliumOption("customDateTimeFormat");
|
||||
|
||||
useEffect(() => {
|
||||
toggleBodyClass("heading-style-", headingStyle);
|
||||
}, [ headingStyle ]);
|
||||
}, [headingStyle]);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("heading_style.title")}>
|
||||
<FormRadioGroup
|
||||
name="heading-style"
|
||||
currentValue={headingStyle} onChange={setHeadingStyle}
|
||||
values={[
|
||||
{ value: "plain", label: t("heading_style.plain") },
|
||||
{ value: "underline", label: t("heading_style.underline") },
|
||||
{ value: "markdown", label: t("heading_style.markdown") }
|
||||
]}
|
||||
/>
|
||||
<OptionsSection title={t("text_editor.title")}>
|
||||
<OptionsRow name="heading-style" label={t("heading_style.title")} description={t("heading_style.description")}>
|
||||
<HeadingStyleSelector currentValue={headingStyle} onChange={setHeadingStyle} />
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow name="auto-readonly-size-text" label={t("text_auto_read_only_size.label")} description={t("text_auto_read_only_size.description")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min={0}
|
||||
unit={t("text_auto_read_only_size.unit")}
|
||||
currentValue={autoReadonlySize}
|
||||
onBlur={setAutoReadonlySize}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow
|
||||
name="custom-date-time-format"
|
||||
label={t("custom_date_time_format.title")}
|
||||
description={<>{t("custom_date_time_format.description_short")} {t("custom_date_time_format.preview", { preview: formatDateTime(new Date(), customDateTimeFormat) })}</>}
|
||||
>
|
||||
<FormTextBox
|
||||
placeholder="YYYY-MM-DD HH:mm"
|
||||
currentValue={customDateTimeFormat || "YYYY-MM-DD HH:mm"} onBlur={setCustomDateTimeFormat}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
const HEADING_STYLES = [
|
||||
{ value: "plain", labelKey: "heading_style.plain" },
|
||||
{ value: "underline", labelKey: "heading_style.underline" },
|
||||
{ value: "markdown", labelKey: "heading_style.markdown" }
|
||||
] as const;
|
||||
|
||||
function HeadingStyleSelector({ currentValue, onChange }: { currentValue: string, onChange: (value: string) => void }) {
|
||||
const currentStyle = HEADING_STYLES.find(s => s.value === currentValue) ?? HEADING_STYLES[0];
|
||||
|
||||
return (
|
||||
<Dropdown text={t(currentStyle.labelKey)}>
|
||||
{HEADING_STYLES.map(({ value, labelKey }) => (
|
||||
<FormListItem
|
||||
key={value}
|
||||
onClick={() => onChange(value)}
|
||||
selected={currentValue === value}
|
||||
>
|
||||
<div className="heading-style-preview">
|
||||
<HeadingPreview style={value} />
|
||||
<span className="heading-style-label">{t(labelKey)}</span>
|
||||
</div>
|
||||
</FormListItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function HeadingPreview({ style }: { style: string }) {
|
||||
const previewClass = `heading-preview heading-preview-${style}`;
|
||||
return (
|
||||
<span className={previewClass}>
|
||||
{style === "markdown" && <span className="heading-prefix">## </span>}
|
||||
Aa
|
||||
{style === "underline" && <span className="heading-underline" />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlockStyle() {
|
||||
const themes = useMemo(() => {
|
||||
const darkThemes: ThemeData[] = [];
|
||||
@@ -159,26 +267,23 @@ function CodeBlockStyle() {
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("highlighting.title")}>
|
||||
<div className="row" style={{ marginBottom: "15px" }}>
|
||||
<FormGroup name="theme" className="col-md-6" label={t("highlighting.color-scheme")} style={{ marginBottom: 0 }}>
|
||||
<FormSelectWithGroups
|
||||
values={themes}
|
||||
keyProperty="val" titleProperty="title"
|
||||
currentValue={codeBlockTheme} onChange={(newTheme) => {
|
||||
loadHighlightingTheme(newTheme);
|
||||
setCodeBlockTheme(newTheme);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<OptionsRow name="code-block-theme" label={t("highlighting.color-scheme")}>
|
||||
<FormSelectWithGroups
|
||||
values={themes}
|
||||
keyProperty="val" titleProperty="title"
|
||||
currentValue={codeBlockTheme} onChange={(newTheme) => {
|
||||
loadHighlightingTheme(newTheme);
|
||||
setCodeBlockTheme(newTheme);
|
||||
}}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<Column md={6} className="side-checkbox">
|
||||
<FormCheckbox
|
||||
name="word-wrap"
|
||||
label={t("code_block.word_wrapping")}
|
||||
currentValue={codeBlockWordWrap} onChange={setCodeBlockWordWrap}
|
||||
/>
|
||||
</Column>
|
||||
</div>
|
||||
<OptionsRowWithToggle
|
||||
name="code-block-word-wrap"
|
||||
label={t("code_block.word_wrapping")}
|
||||
currentValue={codeBlockWordWrap}
|
||||
onChange={setCodeBlockWordWrap}
|
||||
/>
|
||||
|
||||
<CodeBlockPreview theme={codeBlockTheme} wordWrap={codeBlockWordWrap} />
|
||||
</OptionsSection>
|
||||
@@ -303,35 +408,3 @@ export function HighlightsListOptions() {
|
||||
);
|
||||
}
|
||||
|
||||
function DateTimeFormatOptions() {
|
||||
const [ customDateTimeFormat, setCustomDateTimeFormat ] = useTriliumOption("customDateTimeFormat");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("custom_date_time_format.title")}>
|
||||
<FormText>
|
||||
<Trans
|
||||
i18nKey="custom_date_time_format.description"
|
||||
components={{
|
||||
shortcut: <KeyboardShortcut actionName="insertDateTimeToText" /> as React.ReactElement,
|
||||
doc: <a href="https://day.js.org/docs/en/display/format" target="_blank" rel="noopener noreferrer" /> as React.ReactElement
|
||||
}}
|
||||
/>
|
||||
</FormText>
|
||||
|
||||
<div className="row align-items-center">
|
||||
<FormGroup name="custom-date-time-format" className="col-md-6" label={t("custom_date_time_format.format_string")}>
|
||||
<FormTextBox
|
||||
placeholder="YYYY-MM-DD HH:mm"
|
||||
currentValue={customDateTimeFormat || "YYYY-MM-DD HH:mm"} onChange={setCustomDateTimeFormat}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="formatted-date" className="col-md-6" label={t("custom_date_time_format.formatted_time")}>
|
||||
<div>
|
||||
{formatDateTime(new Date(), customDateTimeFormat)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import App from "./support/app";
|
||||
test("Native Title Bar not displayed on web", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsAppearance" });
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Theme" })).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "User Interface" })).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
|
||||
});
|
||||
|
||||
test("Tray settings not displayed on web", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsOther" });
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Deleted Notes" })).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||
});
|
||||
|
||||
|
||||
@@ -212,6 +212,9 @@
|
||||
"button": "Login",
|
||||
"sign_in_with_sso": "Sign in with {{ ssoIssuerName }}"
|
||||
},
|
||||
"password": {
|
||||
"incorrect": "The password you entered is incorrect."
|
||||
},
|
||||
"set_password": {
|
||||
"title": "Set Password",
|
||||
"heading": "Set password",
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
import type { Request, Response } from "express";
|
||||
import optionService from "../../services/options.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
|
||||
const SYSTEM_SANS_SERIF = [
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"Cantarell",
|
||||
"Ubuntu",
|
||||
"Noto Sans",
|
||||
"Helvetica",
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji"
|
||||
].join(",");
|
||||
|
||||
const SYSTEM_MONOSPACE = ["ui-monospace", "SFMono-Regular", "SF Mono", "Consolas", "Source Code Pro", "Ubuntu Mono", "Menlo", "Liberation Mono", "monospace"].join(",");
|
||||
import { SYSTEM_MONOSPACE_FONT_STACK, SYSTEM_SANS_SERIF_FONT_STACK, type OptionMap } from "@triliumnext/commons";
|
||||
|
||||
function getFontCss(req: Request, res: Response) {
|
||||
res.setHeader("Content-Type", "text/css");
|
||||
@@ -44,19 +27,19 @@ function getFontFamily({ mainFontFamily, treeFontFamily, detailFontFamily, monos
|
||||
|
||||
// System override
|
||||
if (mainFontFamily === "system") {
|
||||
mainFontFamily = SYSTEM_SANS_SERIF;
|
||||
mainFontFamily = SYSTEM_SANS_SERIF_FONT_STACK;
|
||||
}
|
||||
|
||||
if (treeFontFamily === "system") {
|
||||
treeFontFamily = SYSTEM_SANS_SERIF;
|
||||
treeFontFamily = SYSTEM_SANS_SERIF_FONT_STACK;
|
||||
}
|
||||
|
||||
if (detailFontFamily === "system") {
|
||||
detailFontFamily = SYSTEM_SANS_SERIF;
|
||||
detailFontFamily = SYSTEM_SANS_SERIF_FONT_STACK;
|
||||
}
|
||||
|
||||
if (monospaceFontFamily === "system") {
|
||||
monospaceFontFamily = SYSTEM_MONOSPACE;
|
||||
monospaceFontFamily = SYSTEM_MONOSPACE_FONT_STACK;
|
||||
}
|
||||
|
||||
// Apply the font override if not using theme fonts.
|
||||
|
||||
@@ -15,6 +15,7 @@ import etapiTokenService from "../../services/etapi_tokens.js";
|
||||
import type { Request } from "express";
|
||||
import totp from "../../services/totp";
|
||||
import recoveryCodeService from "../../services/encryption/recovery_codes";
|
||||
import { t } from "i18next";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -126,7 +127,7 @@ function loginToProtectedSession(req: Request) {
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Given current password doesn't match hash"
|
||||
message: t("password.incorrect")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ interface UserTheme {
|
||||
val: string; // value of the theme, used in the URL
|
||||
title: string; // title of the theme, displayed in the UI
|
||||
noteId: string; // ID of the note containing the theme
|
||||
icon: string; // icon class of the note
|
||||
}
|
||||
|
||||
// options allowed to be updated directly in the Options dialog
|
||||
@@ -180,16 +181,18 @@ function getUserThemes() {
|
||||
const ret: UserTheme[] = [];
|
||||
|
||||
for (const note of notes) {
|
||||
const title = note.getTitleOrProtected();
|
||||
let value = note.getOwnedLabelValue("appTheme");
|
||||
|
||||
if (!value) {
|
||||
value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, "-");
|
||||
value = title.toLowerCase().replace(/[^a-z0-9]/gi, "-");
|
||||
}
|
||||
|
||||
ret.push({
|
||||
val: value,
|
||||
title: note.title,
|
||||
noteId: note.noteId
|
||||
title,
|
||||
noteId: note.noteId,
|
||||
icon: note.getIcon()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import myScryptService from "./my_scrypt.js";
|
||||
import { randomSecureToken, toBase64 } from "../utils.js";
|
||||
import passwordEncryptionService from "./password_encryption.js";
|
||||
import { ChangePasswordResponse } from "@triliumnext/commons";
|
||||
import { t } from "i18next";
|
||||
|
||||
function isPasswordSet() {
|
||||
return !!sql.getValue("SELECT value FROM options WHERE name = 'passwordVerificationHash'");
|
||||
@@ -17,7 +18,7 @@ function changePassword(currentPassword: string, newPassword: string): ChangePas
|
||||
if (!passwordEncryptionService.verifyPassword(currentPassword)) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Given current password doesn't match hash"
|
||||
message: t("password.incorrect")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,41 @@ type KeyboardShortcutsOptions<T extends KeyboardActionNames> = {
|
||||
|
||||
export type FontFamily = "theme" | "serif" | "sans-serif" | "monospace" | string;
|
||||
|
||||
/**
|
||||
* System sans-serif font stack for cross-platform compatibility.
|
||||
* Used when the user selects "System default" for non-monospace fonts.
|
||||
*/
|
||||
export const SYSTEM_SANS_SERIF_FONT_STACK = [
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"Cantarell",
|
||||
"Ubuntu",
|
||||
"Noto Sans",
|
||||
"Helvetica",
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji"
|
||||
].join(",");
|
||||
|
||||
/**
|
||||
* System monospace font stack for cross-platform compatibility.
|
||||
* Used when the user selects "System default" for monospace fonts.
|
||||
*/
|
||||
export const SYSTEM_MONOSPACE_FONT_STACK = [
|
||||
"ui-monospace",
|
||||
"SFMono-Regular",
|
||||
"SF Mono",
|
||||
"Consolas",
|
||||
"Source Code Pro",
|
||||
"Ubuntu Mono",
|
||||
"Menlo",
|
||||
"Liberation Mono",
|
||||
"monospace"
|
||||
].join(",");
|
||||
|
||||
export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActionNames> {
|
||||
openNoteContexts: string;
|
||||
lastDailyBackupDate: string;
|
||||
|
||||
Reference in New Issue
Block a user