Merge pull request #1319 from TriliumNext/feature/rtl
Right-to-left support
| @ -369,7 +369,8 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> | ||||
| 
 | ||||
|         const { note, viewScope } = this; | ||||
| 
 | ||||
|         let title = viewScope?.viewMode === "default" ? note.title : `${note.title}: ${viewScope?.viewMode}`; | ||||
|         const isNormalView = (viewScope?.viewMode === "default" || viewScope?.viewMode === "contextual-help"); | ||||
|         let title = (isNormalView ? note.title : `${note.title}: ${viewScope?.viewMode}`); | ||||
| 
 | ||||
|         if (viewScope?.attachmentId) { | ||||
|             // assuming the attachment has been already loaded
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
| 	"formatVersion": 2, | ||||
| 	"appVersion": "0.92.0-beta", | ||||
| 	"appVersion": "0.92.2-beta", | ||||
| 	"files": [ | ||||
| 		{ | ||||
| 			"isClone": false, | ||||
| @ -34,7 +34,7 @@ | ||||
| 						"OkOZllzB3fqN", | ||||
| 						"yoAe4jV2yzbd" | ||||
| 					], | ||||
| 					"title": "Features", | ||||
| 					"title": "New Features", | ||||
| 					"notePosition": 40, | ||||
| 					"prefix": null, | ||||
| 					"isExpanded": false, | ||||
| @ -47,53 +47,91 @@ | ||||
| 							"value": "bx bx-star", | ||||
| 							"isInheritable": false, | ||||
| 							"position": 10 | ||||
| 						}, | ||||
| 						{ | ||||
| 							"type": "label", | ||||
| 							"name": "sorted", | ||||
| 							"value": "dateCreated", | ||||
| 							"isInheritable": false, | ||||
| 							"position": 20 | ||||
| 						}, | ||||
| 						{ | ||||
| 							"type": "label", | ||||
| 							"name": "sortDirection", | ||||
| 							"value": "desc", | ||||
| 							"isInheritable": false, | ||||
| 							"position": 30 | ||||
| 						} | ||||
| 					], | ||||
| 					"format": "html", | ||||
| 					"attachments": [], | ||||
| 					"dirFileName": "Features", | ||||
| 					"dirFileName": "New Features", | ||||
| 					"children": [ | ||||
| 						{ | ||||
| 							"isClone": false, | ||||
| 							"noteId": "13D1lOc9sqmZ", | ||||
| 							"noteId": "3I277VKYxWDH", | ||||
| 							"notePath": [ | ||||
| 								"OkOZllzB3fqN", | ||||
| 								"yoAe4jV2yzbd", | ||||
| 								"13D1lOc9sqmZ" | ||||
| 								"3I277VKYxWDH" | ||||
| 							], | ||||
| 							"title": "Export as PDF", | ||||
| 							"notePosition": 20, | ||||
| 							"title": "Right-to-left text notes", | ||||
| 							"notePosition": 10, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": false, | ||||
| 							"type": "text", | ||||
| 							"mime": "text/html", | ||||
| 							"attributes": [], | ||||
| 							"attributes": [ | ||||
| 								{ | ||||
| 									"type": "label", | ||||
| 									"name": "iconClass", | ||||
| 									"value": "bx bx-align-right", | ||||
| 									"isInheritable": false, | ||||
| 									"position": 10 | ||||
| 								} | ||||
| 							], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Export as PDF.html", | ||||
| 							"dataFileName": "Right-to-left text notes.html", | ||||
| 							"attachments": [ | ||||
| 								{ | ||||
| 									"attachmentId": "xsGM34t8ssKV", | ||||
| 									"attachmentId": "PSBNAvDyj5Vy", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "Export as PDF_image.png" | ||||
| 									"dataFileName": "Right-to-left text notes_i.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "cvyes4f1Vhmm", | ||||
| 									"attachmentId": "YXYIJznak915", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "1_Export as PDF_image.png" | ||||
| 									"dataFileName": "1_Right-to-left text notes_i.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "b3v1pLE6TF1Y", | ||||
| 									"attachmentId": "Do0S17lDl7uu", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "2_Export as PDF_image.png" | ||||
| 									"dataFileName": "2_Right-to-left text notes_i.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "D3lyhPvPvocb", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "3_Right-to-left text notes_i.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "Tu7llk3GgRkA", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "4_Right-to-left text notes_i.png" | ||||
| 								} | ||||
| 							] | ||||
| 						}, | ||||
| @ -106,12 +144,20 @@ | ||||
| 								"B3YLYM4erjnW" | ||||
| 							], | ||||
| 							"title": "Zen mode", | ||||
| 							"notePosition": 30, | ||||
| 							"notePosition": 20, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": false, | ||||
| 							"type": "text", | ||||
| 							"mime": "text/html", | ||||
| 							"attributes": [], | ||||
| 							"attributes": [ | ||||
| 								{ | ||||
| 									"type": "label", | ||||
| 									"name": "iconClass", | ||||
| 									"value": "bx bxs-yin-yang", | ||||
| 									"isInheritable": false, | ||||
| 									"position": 10 | ||||
| 								} | ||||
| 							], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Zen mode.html", | ||||
| 							"attachments": [ | ||||
| @ -180,6 +226,50 @@ | ||||
| 									"dataFileName": "7_Zen mode_image.png" | ||||
| 								} | ||||
| 							] | ||||
| 						}, | ||||
| 						{ | ||||
| 							"isClone": false, | ||||
| 							"noteId": "13D1lOc9sqmZ", | ||||
| 							"notePath": [ | ||||
| 								"OkOZllzB3fqN", | ||||
| 								"yoAe4jV2yzbd", | ||||
| 								"13D1lOc9sqmZ" | ||||
| 							], | ||||
| 							"title": "Export as PDF", | ||||
| 							"notePosition": 30, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": false, | ||||
| 							"type": "text", | ||||
| 							"mime": "text/html", | ||||
| 							"attributes": [ | ||||
| 								{ | ||||
| 									"type": "label", | ||||
| 									"name": "iconClass", | ||||
| 									"value": "bx bxs-file-pdf", | ||||
| 									"isInheritable": false, | ||||
| 									"position": 30 | ||||
| 								} | ||||
| 							], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Export as PDF.html", | ||||
| 							"attachments": [ | ||||
| 								{ | ||||
| 									"attachmentId": "xsGM34t8ssKV", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "Export as PDF_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "b3v1pLE6TF1Y", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "1_Export as PDF_image.png" | ||||
| 								} | ||||
| 							] | ||||
| 						} | ||||
| 					] | ||||
| 				}, | ||||
| @ -233,8 +323,47 @@ | ||||
| 								} | ||||
| 							], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Text.html", | ||||
| 							"attachments": [] | ||||
| 							"attachments": [], | ||||
| 							"dirFileName": "Text", | ||||
| 							"children": [ | ||||
| 								{ | ||||
| 									"isClone": false, | ||||
| 									"noteId": "B0lcI9xz1r8K", | ||||
| 									"notePath": [ | ||||
| 										"OkOZllzB3fqN", | ||||
| 										"wmegHv51MJMd", | ||||
| 										"crJtzsol4olb", | ||||
| 										"B0lcI9xz1r8K" | ||||
| 									], | ||||
| 									"title": "Content language", | ||||
| 									"notePosition": 10, | ||||
| 									"prefix": null, | ||||
| 									"isExpanded": false, | ||||
| 									"type": "text", | ||||
| 									"mime": "text/html", | ||||
| 									"attributes": [ | ||||
| 										{ | ||||
| 											"type": "relation", | ||||
| 											"name": "internalLink", | ||||
| 											"value": "3I277VKYxWDH", | ||||
| 											"isInheritable": false, | ||||
| 											"position": 10 | ||||
| 										} | ||||
| 									], | ||||
| 									"format": "html", | ||||
| 									"dataFileName": "Content language.html", | ||||
| 									"attachments": [ | ||||
| 										{ | ||||
| 											"attachmentId": "OpIv6CnYCLVa", | ||||
| 											"title": "image.png", | ||||
| 											"role": "image", | ||||
| 											"mime": "image/png", | ||||
| 											"position": 10, | ||||
| 											"dataFileName": "Content language_image.png" | ||||
| 										} | ||||
| 									] | ||||
| 								} | ||||
| 							] | ||||
| 						}, | ||||
| 						{ | ||||
| 							"isClone": false, | ||||
| @ -382,7 +511,7 @@ | ||||
| 							"title": "Book", | ||||
| 							"notePosition": 70, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": true, | ||||
| 							"isExpanded": false, | ||||
| 							"type": "text", | ||||
| 							"mime": "text/html", | ||||
| 							"attributes": [ | ||||
| @ -576,6 +705,14 @@ | ||||
| 											"mime": "image/png", | ||||
| 											"position": 10, | ||||
| 											"dataFileName": "18_Calendar View_image.png" | ||||
| 										}, | ||||
| 										{ | ||||
| 											"attachmentId": "JM6AU8N4MIgB", | ||||
| 											"title": "image.png", | ||||
| 											"role": "image", | ||||
| 											"mime": "image/png", | ||||
| 											"position": 10, | ||||
| 											"dataFileName": "19_Calendar View_image.png" | ||||
| 										} | ||||
| 									] | ||||
| 								} | ||||
| @ -697,7 +834,7 @@ | ||||
| 								"wmegHv51MJMd", | ||||
| 								"foPEtsL51pD2" | ||||
| 							], | ||||
| 							"title": "Geo Map", | ||||
| 							"title": "Geo map", | ||||
| 							"notePosition": 120, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": false, | ||||
| @ -713,23 +850,15 @@ | ||||
| 								} | ||||
| 							], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Geo Map.html", | ||||
| 							"dataFileName": "Geo map.html", | ||||
| 							"attachments": [ | ||||
| 								{ | ||||
| 									"attachmentId": "J0baLTpafs7C", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "Geo Map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "kcYjOvJDFkbS", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "1_Geo Map_image.png" | ||||
| 									"dataFileName": "Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "FDP3JzIVSnuJ", | ||||
| @ -737,7 +866,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "2_Geo Map_image.png" | ||||
| 									"dataFileName": "1_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "eUrcqc8RRuZG", | ||||
| @ -745,7 +874,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "3_Geo Map_image.png" | ||||
| 									"dataFileName": "2_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "1quk4yxJpeHZ", | ||||
| @ -753,7 +882,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "4_Geo Map_image.png" | ||||
| 									"dataFileName": "3_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "iSpyhQ5Ya6Nk", | ||||
| @ -761,7 +890,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "5_Geo Map_image.png" | ||||
| 									"dataFileName": "4_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "ut6vm2aXVfXI", | ||||
| @ -769,7 +898,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "6_Geo Map_image.png" | ||||
| 									"dataFileName": "5_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "uYdb9wWf5Nuv", | ||||
| @ -777,15 +906,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "7_Geo Map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "GhHYO2LteDmZ", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "8_Geo Map_image.png" | ||||
| 									"dataFileName": "6_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "viN50n5G4kB0", | ||||
| @ -793,7 +914,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "9_Geo Map_image.png" | ||||
| 									"dataFileName": "7_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "mgwGrtQZjxxb", | ||||
| @ -801,7 +922,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "10_Geo Map_image.png" | ||||
| 									"dataFileName": "8_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "PMqmCbNLlZOG", | ||||
| @ -809,7 +930,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "11_Geo Map_image.png" | ||||
| 									"dataFileName": "9_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "0AwaQMqt3FVA", | ||||
| @ -817,7 +938,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "12_Geo Map_image.png" | ||||
| 									"dataFileName": "10_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "gR2c2Thmfy3I", | ||||
| @ -825,7 +946,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "13_Geo Map_image.png" | ||||
| 									"dataFileName": "11_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "JULizn130rVI", | ||||
| @ -833,7 +954,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "14_Geo Map_image.png" | ||||
| 									"dataFileName": "12_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "MdC0DpifJwu4", | ||||
| @ -841,7 +962,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "15_Geo Map_image.png" | ||||
| 									"dataFileName": "13_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "gFR2Izzp18LQ", | ||||
| @ -849,7 +970,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "16_Geo Map_image.png" | ||||
| 									"dataFileName": "14_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "42AncDs7SSAf", | ||||
| @ -857,15 +978,7 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "17_Geo Map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "pKdtiq4r0eFY", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "18_Geo Map_image.png" | ||||
| 									"dataFileName": "15_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "FXRVvYpOxWyR", | ||||
| @ -873,7 +986,23 @@ | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "19_Geo Map_image.png" | ||||
| 									"dataFileName": "16_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "qudP7UCtwIq3", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/jpg", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "17_Geo map_image.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "utecGxWk08QY", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "18_Geo map_image.png" | ||||
| 								} | ||||
| 							] | ||||
| 						} | ||||
| @ -943,173 +1072,6 @@ | ||||
| 						} | ||||
| 					] | ||||
| 				}, | ||||
| 				{ | ||||
| 					"isClone": false, | ||||
| 					"noteId": "DtJJ20yEozPA", | ||||
| 					"notePath": [ | ||||
| 						"OkOZllzB3fqN", | ||||
| 						"DtJJ20yEozPA" | ||||
| 					], | ||||
| 					"title": "Theme development", | ||||
| 					"notePosition": 130, | ||||
| 					"prefix": null, | ||||
| 					"isExpanded": false, | ||||
| 					"type": "text", | ||||
| 					"mime": "text/html", | ||||
| 					"attributes": [ | ||||
| 						{ | ||||
| 							"type": "label", | ||||
| 							"name": "iconClass", | ||||
| 							"value": "bx bx-palette", | ||||
| 							"isInheritable": false, | ||||
| 							"position": 10 | ||||
| 						} | ||||
| 					], | ||||
| 					"format": "html", | ||||
| 					"attachments": [], | ||||
| 					"dirFileName": "Theme development", | ||||
| 					"children": [ | ||||
| 						{ | ||||
| 							"isClone": false, | ||||
| 							"noteId": "5HH79ztN0fZA", | ||||
| 							"notePath": [ | ||||
| 								"OkOZllzB3fqN", | ||||
| 								"DtJJ20yEozPA", | ||||
| 								"5HH79ztN0fZA" | ||||
| 							], | ||||
| 							"title": "Creating a custom theme", | ||||
| 							"notePosition": 10, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": false, | ||||
| 							"type": "text", | ||||
| 							"mime": "text/html", | ||||
| 							"attributes": [ | ||||
| 								{ | ||||
| 									"type": "relation", | ||||
| 									"name": "internalLink", | ||||
| 									"value": "aH8Dk5aMiq7R", | ||||
| 									"isInheritable": false, | ||||
| 									"position": 10 | ||||
| 								} | ||||
| 							], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Creating a custom theme.html", | ||||
| 							"attachments": [ | ||||
| 								{ | ||||
| 									"attachmentId": "AJHVfQtIQgJ7", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "Creating a custom theme_im.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "gXLyv5KXjfxg", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "1_Creating a custom theme_im.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "on1gD7BzCWdN", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "2_Creating a custom theme_im.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "17p6z24yW5eP", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "3_Creating a custom theme_im.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "K3cdwj8f90m0", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "4_Creating a custom theme_im.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "bn93hwF7C8sR", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "5_Creating a custom theme_im.png" | ||||
| 								} | ||||
| 							] | ||||
| 						}, | ||||
| 						{ | ||||
| 							"isClone": false, | ||||
| 							"noteId": "aH8Dk5aMiq7R", | ||||
| 							"notePath": [ | ||||
| 								"OkOZllzB3fqN", | ||||
| 								"DtJJ20yEozPA", | ||||
| 								"aH8Dk5aMiq7R" | ||||
| 							], | ||||
| 							"title": "Customize the Next theme", | ||||
| 							"notePosition": 20, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": false, | ||||
| 							"type": "text", | ||||
| 							"mime": "text/html", | ||||
| 							"attributes": [], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Customize the Next theme.html", | ||||
| 							"attachments": [ | ||||
| 								{ | ||||
| 									"attachmentId": "5z4bC0x0eH0P", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "Customize the Next theme_i.png" | ||||
| 								}, | ||||
| 								{ | ||||
| 									"attachmentId": "u0zkXkD7rGXA", | ||||
| 									"title": "image.png", | ||||
| 									"role": "image", | ||||
| 									"mime": "image/png", | ||||
| 									"position": 10, | ||||
| 									"dataFileName": "1_Customize the Next theme_i.png" | ||||
| 								} | ||||
| 							] | ||||
| 						}, | ||||
| 						{ | ||||
| 							"isClone": false, | ||||
| 							"noteId": "pMq6N1oBV9oo", | ||||
| 							"notePath": [ | ||||
| 								"OkOZllzB3fqN", | ||||
| 								"DtJJ20yEozPA", | ||||
| 								"pMq6N1oBV9oo" | ||||
| 							], | ||||
| 							"title": "Reference", | ||||
| 							"notePosition": 30, | ||||
| 							"prefix": null, | ||||
| 							"isExpanded": false, | ||||
| 							"type": "text", | ||||
| 							"mime": "text/html", | ||||
| 							"attributes": [ | ||||
| 								{ | ||||
| 									"type": "relation", | ||||
| 									"name": "internalLink", | ||||
| 									"value": "po38jIc0LD2H", | ||||
| 									"isInheritable": false, | ||||
| 									"position": 10 | ||||
| 								} | ||||
| 							], | ||||
| 							"format": "html", | ||||
| 							"dataFileName": "Reference.html", | ||||
| 							"attachments": [] | ||||
| 						} | ||||
| 					] | ||||
| 				}, | ||||
| 				{ | ||||
| 					"isClone": false, | ||||
| 					"noteId": "LTnkDnYmmZ7s", | ||||
| @ -1283,7 +1245,7 @@ | ||||
| 									"title": "ETAPI", | ||||
| 									"notePosition": 10, | ||||
| 									"prefix": null, | ||||
| 									"isExpanded": true, | ||||
| 									"isExpanded": false, | ||||
| 									"type": "text", | ||||
| 									"mime": "text/html", | ||||
| 									"attributes": [], | ||||
| @ -1333,7 +1295,7 @@ | ||||
| 									"title": "Internal API", | ||||
| 									"notePosition": 20, | ||||
| 									"prefix": null, | ||||
| 									"isExpanded": true, | ||||
| 									"isExpanded": false, | ||||
| 									"type": "text", | ||||
| 									"mime": "text/html", | ||||
| 									"attributes": [], | ||||
|  | ||||
| Before Width: | Height: | Size: 26 KiB | 
| Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 340 B | 
| After Width: | Height: | Size: 89 KiB | 
| Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB | 
| After Width: | Height: | Size: 115 KiB | 
| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
| After Width: | Height: | Size: 116 KiB | 
| Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB | 
| After Width: | Height: | Size: 86 KiB | 
| Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB | 
| Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB | 
| Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB | 
| Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB | 
| @ -23,7 +23,7 @@ | ||||
|           as PDF. On the server or PWA (mobile), the option is not available due | ||||
|           to technical constraints and it will be hidden.</p> | ||||
|         <p>To print a note, select the | ||||
|           <img src="2_Export as PDF_image.png" width="29" | ||||
|           <img src="1_Export as PDF_image.png" width="29" | ||||
|           height="31">button to the right of the note and select <i>Export as PDF</i>.</p> | ||||
|         <p>Afterwards you will be prompted to select where to save the PDF file. | ||||
|           Upon confirmation, the resulting PDF will be opened automatically using | ||||
| @ -33,7 +33,7 @@ | ||||
|           <a | ||||
|           href="#root/OeKBfN6JbMIq/jRV1MPt4mNSP/hrC6xn7hnDq5">report the issue</a>. In this case, it's best to offer a sample note (click | ||||
|             on the | ||||
|             <img src="2_Export as PDF_image.png" width="29" height="31">button, select Export note → This note and all of its descendants → HTML | ||||
|             <img src="1_Export as PDF_image.png" width="29" height="31">button, select Export note → This note and all of its descendants → HTML | ||||
|             in ZIP archive). Make sure not to accidentally leak any personal information.</p> | ||||
|         <h2>Landscape mode</h2> | ||||
|         <p>When exporting to PDF, there are no customizable settings such as page | ||||
| Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB | 
| @ -0,0 +1,56 @@ | ||||
| <html> | ||||
|    | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" href="../../style.css"> | ||||
|     <base target="_parent"> | ||||
|     <title data-trilium-title>Right-to-left text notes</title> | ||||
|   </head> | ||||
|    | ||||
|   <body> | ||||
|     <div class="content"> | ||||
|        <h1 data-trilium-h1>Right-to-left text notes</h1> | ||||
| 
 | ||||
|       <div class="ck-content"> | ||||
|         <p>Trilium now has basic support for right-to-left text, at note level.</p> | ||||
|         <figure | ||||
|         class="table"> | ||||
|           <table> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:906/557;" src="3_Right-to-left text notes_i.png" | ||||
|                     width="906" height="557"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:906/557;" src="2_Right-to-left text notes_i.png" | ||||
|                     width="906" height="557"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|           </figure> | ||||
|           <p>Note that only the Text note type supports this.</p> | ||||
|           <p>The list of languages is configurable via the a new dedicated settings | ||||
|             page:</p> | ||||
|           <figure class="image"> | ||||
|             <img style="aspect-ratio:1248/635;" src="4_Right-to-left text notes_i.png" | ||||
|             width="1248" height="635"> | ||||
|           </figure> | ||||
|           <p>To select the corresponding language of the text, go to “Basic Properties” | ||||
|             and select your desired language.</p> | ||||
|           <p> | ||||
|             <img src="1_Right-to-left text notes_i.png" width="635" height="492"> | ||||
|           </p> | ||||
|           <p>Feel free to report any issues regarding right to left support.</p> | ||||
|           <p> </p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| After Width: | Height: | Size: 100 KiB | 
| Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB | 
| Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB | 
| Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB | 
| Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB | 
| Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB | 
| Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 191 KiB | 
| Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB | 
| After Width: | Height: | Size: 31 KiB | 
| Before Width: | Height: | Size: 36 KiB | 
| After Width: | Height: | Size: 100 KiB | 
| Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB | 
| Before Width: | Height: | Size: 515 KiB After Width: | Height: | Size: 515 KiB | 
| Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 397 KiB After Width: | Height: | Size: 397 KiB | 
| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB | 
| Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB | 
| Before Width: | Height: | Size: 6.9 KiB | 
| Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB | 
| Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB | 
| After Width: | Height: | Size: 4.6 KiB | 
| @ -118,6 +118,12 @@ | ||||
|                 <td>When present (regardless of value), it will show the number of the week | ||||
|                   on the calendar.</td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td><code>~child:template</code> | ||||
|                 </td> | ||||
|                 <td>Defines the template for newly created notes in the calendar (via dragging | ||||
|                   or clicking).</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </figure> | ||||
| @ -175,6 +181,36 @@ | ||||
|                   than the title, either a label (e.g. <code>#assignee</code>) or a relation | ||||
|                   (e.g. <code>~for</code>). See <i>Advanced use-cases</i> for more information.</td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td><code>#calendar:promotedAttributes</code> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                   <p>Allows displaying the value of one or more promoted attributes in the | ||||
|                     calendar like this: | ||||
|                     <img src="19_Calendar View_image.png" width="131" height="113"> | ||||
|                   </p><pre><code class="language-text-x-trilium-auto">#label:weight="promoted,number,single,precision=1" | ||||
| #label:mood="promoted,alias=Mood,single,text" | ||||
| #calendar:promotedAttributes="label:weight,label:mood" </code></pre> | ||||
|                   <p>It can also be used with relations, case in which it will display the | ||||
|                     title of the target note:</p><pre><code class="language-text-x-trilium-auto">#relation:assignee="promoted,alias=Assignee,single,text" | ||||
| #calendar:promotedAttributes="relation:assignee"  | ||||
| ~assignee=@My assignee </code></pre> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td><code>#calendar:startDate</code> | ||||
|                 </td> | ||||
|                 <td>Allows using a different label to represent the start date, other than <code>#startDate</code> (e.g. <code>#expiryDate</code>). | ||||
|                   The label name must be prefixed with <code>#</code>. If the label is not | ||||
|                   defined for a note, the default will be used instead.</td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td><code>#calendar:endDate</code> | ||||
|                 </td> | ||||
|                 <td>Allows using a different label to represent the start date, other than <code>#endDate</code>. | ||||
|                   The label name must be prefixed with <code>#</code>. If the label is not | ||||
|                   defined for a note, the default will be used instead.</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|           </figure> | ||||
|  | ||||
| Before Width: | Height: | Size: 6.5 KiB | 
| @ -5,12 +5,12 @@ | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" href="../../style.css"> | ||||
|     <base target="_parent"> | ||||
|     <title data-trilium-title>Geo Map</title> | ||||
|     <title data-trilium-title>Geo map</title> | ||||
|   </head> | ||||
|    | ||||
|   <body> | ||||
|     <div class="content"> | ||||
|        <h1 data-trilium-h1>Geo Map</h1> | ||||
|        <h1 data-trilium-h1>Geo map</h1> | ||||
| 
 | ||||
|       <div class="ck-content"> | ||||
|         <h2>Creating a new geo map</h2> | ||||
| @ -26,7 +26,7 @@ | ||||
|                 <th>1</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:1256/1044;" src="9_Geo Map_image.png" width="1256" | ||||
|                     <img style="aspect-ratio:1256/1044;" src="7_Geo map_image.png" width="1256" | ||||
|                     height="1044"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -36,7 +36,7 @@ | ||||
|                 <th>2</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:1720/1396;" src="3_Geo Map_image.png" width="1720" | ||||
|                     <img style="aspect-ratio:1720/1396;" src="2_Geo map_image.png" width="1720" | ||||
|                     height="1396"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -69,18 +69,18 @@ | ||||
|                   <p>To create a marker, first navigate to the desired point on the map. Then | ||||
|                     press the | ||||
|                     <img class="image_resized" style="aspect-ratio:72/66;width:7.37%;" | ||||
|                     src="4_Geo Map_image.png" width="72" height="66">button on the top-right of the map.</p> | ||||
|                     src="3_Geo map_image.png" width="72" height="66">button on the top-right of the map.</p> | ||||
|                   <p>If the button is not visible, make sure the button section is visible | ||||
|                     by pressing the chevron button ( | ||||
|                     <img class="image_resized" style="aspect-ratio:72/66;width:7.51%;" | ||||
|                     src="10_Geo Map_image.png" width="72" height="66">) in the top-right of the map.</p> | ||||
|                     src="8_Geo map_image.png" width="72" height="66">) in the top-right of the map.</p> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <th>2</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:1730/416;" src="14_Geo Map_image.png" width="1730" | ||||
|                     <img style="aspect-ratio:1730/416;" src="12_Geo map_image.png" width="1730" | ||||
|                     height="416"> | ||||
|                   </figure> | ||||
|                   <p> </p> | ||||
| @ -96,7 +96,7 @@ | ||||
|                 <th>3</th> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:1586/404;" src="1_Geo Map_image.png" width="1586" | ||||
|                     <img style="aspect-ratio:1586/404;" src="Geo map_image.png" width="1586" | ||||
|                     height="404"> | ||||
|                   </figure> | ||||
|                   <p> </p> | ||||
| @ -107,7 +107,7 @@ | ||||
|                 <th>4</th> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:1696/608;" src="6_Geo Map_image.png" width="1696" | ||||
|                     <img style="aspect-ratio:1696/608;" src="5_Geo map_image.png" width="1696" | ||||
|                     height="608"> | ||||
|                   </figure> | ||||
|                   <p> </p> | ||||
| @ -122,7 +122,7 @@ | ||||
|         <p>The location of a marker is stored in the <code>#geolocation</code> attribute | ||||
|           of the child notes:</p> | ||||
|         <figure class="image"> | ||||
|           <img style="aspect-ratio:1288/278;" src="12_Geo Map_image.png" width="1288" | ||||
|           <img style="aspect-ratio:1288/278;" src="10_Geo map_image.png" width="1288" | ||||
|           height="278"> | ||||
|         </figure> | ||||
|         <p>This value can be added manually if needed. The value of the attribute | ||||
| @ -155,6 +155,13 @@ | ||||
|             </ul> | ||||
|           </li> | ||||
|         </ul> | ||||
|         <h2>Icon and color of the markers</h2> | ||||
|         <p> | ||||
|           <img src="18_Geo map_image.png" alt="image" width="523" height="295"> | ||||
|         </p> | ||||
|         <p>The markers will have the same icon as the note.</p> | ||||
|         <p>It's possible to add a custom color to a marker by assigning them a <code>#color</code> attribute | ||||
|           such as <code>#color=green</code>.</p> | ||||
|         <h2>Adding the coordinates manually</h2> | ||||
|         <p>In a nutshell, create a child note and set the <code>#geolocation</code> attribute | ||||
|           to the coordinates.</p> | ||||
| @ -168,7 +175,7 @@ | ||||
|                 <th>1</th> | ||||
|                 <td> | ||||
|                   <figure class="image image-style-align-center image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:732/918;" src="16_Geo Map_image.png" width="732" | ||||
|                     <img style="aspect-ratio:732/918;" src="14_Geo map_image.png" width="732" | ||||
|                     height="918"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -185,7 +192,7 @@ | ||||
|                 <th>2</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:518/84;" src="19_Geo Map_image.png" width="518" | ||||
|                     <img style="aspect-ratio:518/84;" src="16_Geo map_image.png" width="518" | ||||
|                     height="84"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -199,7 +206,7 @@ | ||||
|                 <th>3</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:1074/276;" src="11_Geo Map_image.png" width="1074" | ||||
|                     <img style="aspect-ratio:1074/276;" src="9_Geo map_image.png" width="1074" | ||||
|                     height="276"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -225,7 +232,7 @@ | ||||
|                 <th>1</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:562/454;" src="17_Geo Map_image.png" width="562" | ||||
|                     <img style="aspect-ratio:562/454;" src="15_Geo map_image.png" width="562" | ||||
|                     height="454"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -236,7 +243,7 @@ | ||||
|                 <th>2</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:696/480;" src="13_Geo Map_image.png" width="696" | ||||
|                     <img style="aspect-ratio:696/480;" src="11_Geo map_image.png" width="696" | ||||
|                     height="480"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -250,7 +257,7 @@ | ||||
|                 <th>3</th> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:640/276;" src="2_Geo Map_image.png" width="640" | ||||
|                     <img style="aspect-ratio:640/276;" src="1_Geo map_image.png" width="640" | ||||
|                     height="276"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -275,7 +282,7 @@ | ||||
|                 <th>1</th> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:226/74;" src="7_Geo Map_image.png" width="226" | ||||
|                     <img style="aspect-ratio:226/74;" src="6_Geo map_image.png" width="226" | ||||
|                     height="74"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -286,7 +293,7 @@ | ||||
|                 <th>2</th> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:322/222;" src="5_Geo Map_image.png" width="322" | ||||
|                     <img style="aspect-ratio:322/222;" src="4_Geo map_image.png" width="322" | ||||
|                     height="222"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -297,7 +304,7 @@ | ||||
|                 <th>3</th> | ||||
|                 <td> | ||||
|                   <figure class="image image_resized" style="width:100%;"> | ||||
|                     <img style="aspect-ratio:620/530;" src="15_Geo Map_image.png" width="620" | ||||
|                     <img style="aspect-ratio:620/530;" src="13_Geo map_image.png" width="620" | ||||
|                     height="530"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
| @ -310,9 +317,16 @@ | ||||
|             </tbody> | ||||
|           </table> | ||||
|           </figure> | ||||
|           <p> </p> | ||||
|           <p> </p> | ||||
|           <p> </p> | ||||
|           <h2>Troubleshooting</h2> | ||||
|           <h3>Grid-like artifacts on the map</h3> | ||||
|           <p> | ||||
|             <img class="image_resized" style="aspect-ratio:678/499;width:58%;" src="17_Geo map_image.png" | ||||
|             width="678" height="499"> | ||||
|           </p> | ||||
|           <p>This occurs if the application is not at 100% zoom which causes the pixels | ||||
|             of the map to not render correctly due to fractional scaling. The only | ||||
|             possible solution i to set the UI zoom at 100% (default keyboard shortcut | ||||
|             is Ctrl+0).</p> | ||||
|           <p> </p> | ||||
|       </div> | ||||
|     </div> | ||||
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB | 
| @ -1,19 +0,0 @@ | ||||
| <html> | ||||
|    | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" href="../../style.css"> | ||||
|     <base target="_parent"> | ||||
|     <title data-trilium-title>Text</title> | ||||
|   </head> | ||||
|    | ||||
|   <body> | ||||
|     <div class="content"> | ||||
|        <h1 data-trilium-h1>Text</h1> | ||||
| 
 | ||||
|       <div class="ck-content"></div> | ||||
|     </div> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,34 @@ | ||||
| <html> | ||||
|    | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" href="../../../style.css"> | ||||
|     <base target="_parent"> | ||||
|     <title data-trilium-title>Content language</title> | ||||
|   </head> | ||||
|    | ||||
|   <body> | ||||
|     <div class="content"> | ||||
|        <h1 data-trilium-h1>Content language</h1> | ||||
| 
 | ||||
|       <div class="ck-content"> | ||||
|         <p>A language hint can be provided for text notes. This option informs the | ||||
|           browser or the desktop application about the language the note is written | ||||
|           in (for example this might help with spellchecking), and it also determines | ||||
|           whether the text is displayed from right-to-left for languages such as | ||||
|           Arabic, Hebrew, etc.</p> | ||||
|         <p>For more information about right-to-left support, see <a class="reference-link" | ||||
|           href="../../New%20Features/Right-to-left%20text%20notes.html">Right-to-left text notes</a>.</p> | ||||
|         <p>To set the language of the content, go to “Basic Properties” and look | ||||
|           for the “Language” field. By default there will be no content languages | ||||
|           set, they can be configured by going to settings or by selecting the “Configure | ||||
|           languages” item in the list.</p> | ||||
|         <p> | ||||
|           <img src="Content language_image.png" width="635" height="492"> | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| After Width: | Height: | Size: 89 KiB | 
| Before Width: | Height: | Size: 26 KiB | 
| Before Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 57 KiB | 
| Before Width: | Height: | Size: 28 KiB | 
| Before Width: | Height: | Size: 69 KiB | 
| Before Width: | Height: | Size: 4.7 KiB | 
| @ -1,94 +0,0 @@ | ||||
| <html> | ||||
|    | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" href="../../style.css"> | ||||
|     <base target="_parent"> | ||||
|     <title data-trilium-title>Creating a custom theme</title> | ||||
|   </head> | ||||
|    | ||||
|   <body> | ||||
|     <div class="content"> | ||||
|        <h1 data-trilium-h1>Creating a custom theme</h1> | ||||
| 
 | ||||
|       <div class="ck-content"> | ||||
|         <h2>Step 1. Find a place to place the themes</h2> | ||||
|         <p>Organization is an important aspect of managing a knowledge base. When | ||||
|           developing a new theme or importing an existing one it's a good idea to | ||||
|           keep them into one place.</p> | ||||
|         <p>As such, the first step is to create a new note to gather all the themes.</p> | ||||
|         <p> | ||||
|           <img src="5_Creating a custom theme_im.png" width="181" height="84"> | ||||
|         </p> | ||||
|         <h2>Step 2. Create the theme</h2> | ||||
|         <figure class="table" style="width:100%;"> | ||||
|           <table class="ck-table-resized"> | ||||
|             <colgroup> | ||||
|               <col style="width:32.47%;"> | ||||
|                 <col style="width:67.53%;"> | ||||
|             </colgroup> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:651/220;" src="3_Creating a custom theme_im.png" | ||||
|                     width="651" height="220"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
|                 <td style="vertical-align:top;">Themes are code notes with a special attribute. Start by creating a new | ||||
|                   code note.</td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:302/349;" src="1_Creating a custom theme_im.png" | ||||
|                     width="302" height="349"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
|                 <td style="vertical-align:top;">Then change the note type to a CSS code.</td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td> | ||||
|                   <figure class="image"> | ||||
|                     <img style="aspect-ratio:316/133;" src="Creating a custom theme_im.png" | ||||
|                     width="316" height="133"> | ||||
|                   </figure> | ||||
|                 </td> | ||||
|                 <td style="vertical-align:top;">In the <i>Owned Attributes</i> section define the <code>#appTheme</code> attribute | ||||
|                   to point to any desired name. This is the name that will show up in the | ||||
|                   appearance section in settings.</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </figure> | ||||
|         <h2>Step 3. Define the theme's CSS</h2> | ||||
|         <p>As a very simple example we will change the background color of the launcher | ||||
|           pane to a shade of blue.</p> | ||||
|         <p>To alter the different variables of the theme:</p><pre><code class="language-text-css">:root { | ||||
| 	--launcher-pane-background-color: #0d6efd; | ||||
| }</code></pre> | ||||
|         <h2>Step 4. Activating the theme</h2> | ||||
|         <p>Refresh the application (Ctrl+Shift+R is a good way to do so) and go to | ||||
|           settings. You should see the newly created theme:</p> | ||||
|         <p> | ||||
|           <img src="2_Creating a custom theme_im.png" width="631" height="481"> | ||||
|         </p> | ||||
|         <p>Afterwards the application will refresh itself with the new theme:</p> | ||||
|         <p> | ||||
|           <img src="4_Creating a custom theme_im.png" width="653" height="554"> | ||||
|         </p> | ||||
|         <p>Do note that the theme will be based off of the legacy theme. To override | ||||
|           that and base the theme on the new TriliumNext theme, see: <a class="reference-link" | ||||
|           href="Customize%20the%20Next%20theme.html">Theme base (legacy vs. next)</a> | ||||
|         </p> | ||||
|         <h2>Step 5. Making changes</h2> | ||||
|         <p>Simply go back to the note and change according to needs. To apply the | ||||
|           changes to the current window, press Ctrl+Shift+R to refresh.</p> | ||||
|         <p>It's a good idea to keep two windows, one for editing and the other one | ||||
|           for previewing the changes.</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| Before Width: | Height: | Size: 11 KiB | 
| @ -1,36 +0,0 @@ | ||||
| <html> | ||||
|    | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" href="../../style.css"> | ||||
|     <base target="_parent"> | ||||
|     <title data-trilium-title>Customize the Next theme</title> | ||||
|   </head> | ||||
|    | ||||
|   <body> | ||||
|     <div class="content"> | ||||
|        <h1 data-trilium-h1>Customize the Next theme</h1> | ||||
| 
 | ||||
|       <div class="ck-content"> | ||||
|         <p>By default, any custom theme will be based on the legacy light theme. | ||||
|           To use the TriliumNext theme instead, add the <code>#appThemeBase=next</code> attribute | ||||
|           onto the existing theme. The <code>appTheme</code> attribute must also be | ||||
|           present.</p> | ||||
|         <p> | ||||
|           <img src="Customize the Next theme_i.png" width="424" height="140"> | ||||
|         </p> | ||||
|         <p>When <code>appThemeBase</code> is set to <code>next</code> it will use the | ||||
|           “TriliumNext (auto)” theme. Any other value is ignored and will use the | ||||
|           legacy white theme instead.</p> | ||||
|         <h2>Overrides</h2> | ||||
|         <p>Do note that the TriliumNext theme has a few more overrides than the legacy | ||||
|           theme, so you might need to suffix <code>!important</code> if the style changes | ||||
|           are not applied.</p><pre><code class="language-text-css">:root { | ||||
| 	--launcher-pane-background-color: #0d6efd !important; | ||||
| }</code></pre> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| Before Width: | Height: | Size: 14 KiB | 
| @ -1,180 +0,0 @@ | ||||
| <html> | ||||
|    | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="stylesheet" href="../../style.css"> | ||||
|     <base target="_parent"> | ||||
|     <title data-trilium-title>Reference</title> | ||||
|   </head> | ||||
|    | ||||
|   <body> | ||||
|     <div class="content"> | ||||
|        <h1 data-trilium-h1>Reference</h1> | ||||
| 
 | ||||
|       <div class="ck-content"> | ||||
|         <h2>Detecting mobile vs. desktop</h2> | ||||
|         <p>The mobile layout is different than the one on the desktop. Use <code>body.mobile</code> and <code>body.desktop</code> to | ||||
|           differentiate between them.</p><pre><code class="language-text-css">body.mobile #root-widget { | ||||
| 	/* Do something on mobile */ | ||||
| } | ||||
| 
 | ||||
| body.desktop #root-widget { | ||||
| 	/* Do something on desktop */ | ||||
| }</code></pre> | ||||
|         <p>Do note that there is also a “tablet mode” in the mobile layout. For that | ||||
|           particular case media queries are required:</p><pre><code class="language-text-css">@media (max-width: 991px) { | ||||
| 
 | ||||
|     #launcher-pane { | ||||
| 
 | ||||
|         /* Do something on mobile layout */ | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @media (min-width: 992px) { | ||||
| 
 | ||||
|     #launcher-pane { | ||||
| 
 | ||||
|         /* Do something on mobile tablet + desktop layout */ | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| }</code></pre> | ||||
|         <h2>Detecting horizontal vs. vertical layout</h2> | ||||
|         <p>The user can select between vertical layout (the classical one, where | ||||
|           the launcher bar is on the left) and a horizontal layout (where the launcher | ||||
|           bar is on the top and tabs are full-width).</p> | ||||
|         <p>Different styles can be applied by using classes at <code>body</code> level:</p><pre><code class="language-text-x-trilium-auto">body.layout-vertical #left-pane { | ||||
| 	/* Do something */ | ||||
| } | ||||
| 
 | ||||
| body.layout-horizontal #center-pane { | ||||
| 	/* Do something else */	 | ||||
| }</code></pre> | ||||
|         <p>The two different layouts use different containers (but they are present | ||||
|           in the DOM regardless of the user's choice), for example <code>#horizontal-main-container</code> and <code>#vertical-main-container</code> can | ||||
|           be used to customize the background of the content section.</p> | ||||
|         <h2>Detecting platform (Windows, macOS) or Electron</h2> | ||||
|         <p>It is possible to add particular styles that only apply to a given platform | ||||
|           by using the classes in <code>body</code>:</p> | ||||
|         <figure class="table"> | ||||
|           <table> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th>Windows</th> | ||||
|                 <th>macOS</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td><pre><code class="language-text-x-trilium-auto">body.platform-win32 { | ||||
| 	background: red; | ||||
| }</code></pre> | ||||
|                 </td> | ||||
|                 <td><pre><code class="language-text-x-trilium-auto">body.platform-darwin { | ||||
| 	background: red; | ||||
| }</code></pre> | ||||
|                 </td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </figure> | ||||
|         <p>It is also possible to only apply a style if running under Electron (desktop | ||||
|           application):</p><pre><code class="language-text-x-trilium-auto">body.electron { | ||||
| 	background: blue; | ||||
| }</code></pre> | ||||
|         <h3>Native title bar</h3> | ||||
|         <p>It's possible to detect if the user has selected the native title bar | ||||
|           or the custom title bar by querying against <code>body</code>:</p><pre><code class="language-text-x-trilium-auto">body.electron.native-titlebar { | ||||
| 	/* Do something */ | ||||
| } | ||||
| 
 | ||||
| body.electron:not(.native-titlebar) { | ||||
| 	/* Do something else */ | ||||
| }</code></pre> | ||||
|         <h3>Native window buttons</h3> | ||||
|         <p>When running under Electron with native title bar off, a feature was introduced | ||||
|           to use the platform-specific window buttons such as the semaphore on macOS.</p> | ||||
|         <p>See <a href="https://github.com/TriliumNext/Notes/pull/702">Native title bar buttons by eliandoran · Pull Request #702 · TriliumNext/Notes</a> for | ||||
|           the original implementation of this feature, including screenshots.</p> | ||||
|         <h4>On Windows</h4> | ||||
|         <p>The colors of the native window button area can be adjusted using a RGB | ||||
|           hex color:</p><pre><code class="language-text-x-trilium-auto">body { | ||||
| 	--native-titlebar-foreground: #ffffff; | ||||
| 	--native-titlebar-background: #ff0000; | ||||
| }</code></pre> | ||||
|         <p>It is also possible to use transparency at the cost of reduced hover colors | ||||
|           using a RGBA hex color:</p><pre><code class="language-text-x-trilium-auto">body { | ||||
| 	--native-titlebar-background: #ff0000aa; | ||||
| }</code></pre> | ||||
|         <p>Note that the value is read when the window is initialized and then it | ||||
|           is refreshed only when the user changes their light/dark mode preference.</p> | ||||
|         <h4>On macOS</h4> | ||||
|         <p>On macOS the semaphore window buttons are enabled by default when the | ||||
|           native title bar is disabled. The offset of the buttons can be adjusted | ||||
|           using:</p><pre><code class="language-text-css">body { | ||||
|     --native-titlebar-darwin-x-offset: 12; | ||||
|     --native-titlebar-darwin-y-offset: 14 !important; | ||||
| }</code></pre> | ||||
|         <h3>Background/transparency effects on Windows (Mica)</h3> | ||||
|         <p>Windows 11 offers a special background/transparency effect called Mica, | ||||
|           which can be enabled by themes by setting the <code>--background-material</code> variable | ||||
|           at <code>body</code> level:</p><pre><code class="language-text-css">body.electron.platform-win32 { | ||||
| 	--background-material: tabbed;  | ||||
| }</code></pre> | ||||
|         <p>The value can be either <code>tabbed</code> (especially useful for the horizontal | ||||
|           layout) or <code>mica</code> (ideal for the vertical layout).</p> | ||||
|         <p>Do note that the Mica effect is applied at <code>body</code> level and the | ||||
|           theme needs to make the entire hierarchy (semi-)transparent in order for | ||||
|           it to be visible. Use the TrilumNext theme as an inspiration.</p> | ||||
|         <h2>Note icons, tab workspace accent color</h2> | ||||
|         <p>Theme capabilities are small adjustments done through CSS variables that | ||||
|           can affect the layout or the visual aspect of the application.</p> | ||||
|         <p>In the tab bar, to display the icons of notes instead of the icon of the | ||||
|           workspace:</p><pre><code class="language-text-css">:root { | ||||
| 	--tab-note-icons: true; | ||||
| }</code></pre> | ||||
|         <p>When a workspace is hoisted for a given tab, it is possible to get the | ||||
|           background color of that workspace, for example to apply a small strip | ||||
|           on the tab instead of the whole background color:</p><pre><code class="language-text-css">.note-tab .note-tab-wrapper { | ||||
|     --tab-background-color: initial !important; | ||||
| } | ||||
| 
 | ||||
| .note-tab .note-tab-wrapper::after { | ||||
|     content: ""; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     height: 3px; | ||||
|     background-color: var(--workspace-tab-background-color); | ||||
| }</code></pre> | ||||
|         <h2>Custom fonts</h2> | ||||
|         <p>Currently the only way to include a custom font is to use <a class="reference-link" | ||||
|           href="../Advanced%20topics/Custom%20resource%20providers.html">Custom resource providers</a>. | ||||
|           Basically import a font into Trilium and assign it <code>#customResourceProvider=fonts/myfont.ttf</code> and | ||||
|           then import the font in CSS via <code>/custom/fonts/myfont.ttf</code>.</p> | ||||
|         <h2>Dark and light themes</h2> | ||||
|         <p>A light theme needs to have the following CSS:</p><pre><code class="language-text-css">:root { | ||||
| 	--theme-style: light; | ||||
| }</code></pre> | ||||
|         <p>if the theme is dark, then <code>--theme-style</code> needs to be <code>dark</code>.</p> | ||||
|         <p>If the theme is auto (e.g. supports both light or dark based on <code>prefers-color-scheme</code>) | ||||
|           it must also declare (in addition to setting <code>--theme-style</code> to | ||||
|           either <code>light</code> or <code>dark</code>):</p><pre><code class="language-text-css">:root { | ||||
| 
 | ||||
|     --theme-style-auto: true; | ||||
| 
 | ||||
| }</code></pre> | ||||
|         <p>This will affect the behavior of the Electron application by informing | ||||
|           the operating system of the color preference (e.g. background effects will | ||||
|           appear correct on Windows).</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| @ -6,6 +6,6 @@ | ||||
| </head> | ||||
| <frameset cols="25%,75%"> | ||||
|     <frame name="navigation" src="navigation.html"> | ||||
|     <frame name="detail" src="User%20Guide/Features/Export%20as%20PDF.html"> | ||||
|     <frame name="detail" src="User%20Guide/New%20Features/Right-to-left%20text%20notes.html"> | ||||
| </frameset> | ||||
| </html> | ||||
| @ -9,17 +9,24 @@ | ||||
|     <ul> | ||||
|       <li>User Guide | ||||
|         <ul> | ||||
|           <li>Features | ||||
|           <li>New Features | ||||
|             <ul> | ||||
|               <li><a href="User%20Guide/Features/Export%20as%20PDF.html" target="detail">Export as PDF</a> | ||||
|               <li><a href="User%20Guide/New%20Features/Right-to-left%20text%20notes.html" | ||||
|                 target="detail">Right-to-left text notes</a> | ||||
|               </li> | ||||
|               <li><a href="User%20Guide/Features/Zen%20mode.html" target="detail">Zen mode</a> | ||||
|               <li><a href="User%20Guide/New%20Features/Zen%20mode.html" target="detail">Zen mode</a> | ||||
|               </li> | ||||
|               <li><a href="User%20Guide/New%20Features/Export%20as%20PDF.html" target="detail">Export as PDF</a> | ||||
|               </li> | ||||
|             </ul> | ||||
|           </li> | ||||
|           <li>Note Types | ||||
|             <ul> | ||||
|               <li><a href="User%20Guide/Note%20Types/Text.html" target="detail">Text</a> | ||||
|               <li>Text | ||||
|                 <ul> | ||||
|                   <li><a href="User%20Guide/Note%20Types/Text/Content%20language.html" target="detail">Content language</a> | ||||
|                   </li> | ||||
|                 </ul> | ||||
|               </li> | ||||
|               <li><a href="User%20Guide/Note%20Types/Code.html" target="detail">Code</a> | ||||
|               </li> | ||||
| @ -45,7 +52,7 @@ | ||||
|               </li> | ||||
|               <li><a href="User%20Guide/Note%20Types/Mind%20Map.html" target="detail">Mind Map</a> | ||||
|               </li> | ||||
|               <li><a href="User%20Guide/Note%20Types/Geo%20Map.html" target="detail">Geo Map</a> | ||||
|               <li><a href="User%20Guide/Note%20Types/Geo%20map.html" target="detail">Geo map</a> | ||||
|               </li> | ||||
|             </ul> | ||||
|           </li> | ||||
| @ -56,18 +63,6 @@ | ||||
|               </li> | ||||
|             </ul> | ||||
|           </li> | ||||
|           <li>Theme development | ||||
|             <ul> | ||||
|               <li><a href="User%20Guide/Theme%20development/Creating%20a%20custom%20theme.html" | ||||
|                 target="detail">Creating a custom theme</a> | ||||
|               </li> | ||||
|               <li><a href="User%20Guide/Theme%20development/Customize%20the%20Next%20theme.html" | ||||
|                 target="detail">Customize the Next theme</a> | ||||
|               </li> | ||||
|               <li><a href="User%20Guide/Theme%20development/Reference.html" target="detail">Reference</a> | ||||
|               </li> | ||||
|             </ul> | ||||
|           </li> | ||||
|           <li>Scripting | ||||
|             <ul> | ||||
|               <li>Examples | ||||
|  | ||||
| @ -23,6 +23,28 @@ async function removeAttributeById(noteId: string, attributeId: string) { | ||||
|     await server.remove(`notes/${noteId}/attributes/${attributeId}`); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy. | ||||
|  * For an attribute with an empty value, pass an empty string instead. | ||||
|  * | ||||
|  * @param note the note to set the attribute to. | ||||
|  * @param type the type of attribute (label or relation). | ||||
|  * @param name the name of the attribute to set. | ||||
|  * @param value the value of the attribute to set. | ||||
|  */ | ||||
| async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { | ||||
|     if (value) { | ||||
|         // Create or update the attribute.
 | ||||
|         await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); | ||||
|     } else { | ||||
|         // Remove the attribute if it exists on the server but we don't define a value for it.
 | ||||
|         const attributeId = note.getAttribute(type, name)?.attributeId; | ||||
|         if (attributeId) { | ||||
|             await server.remove(`notes/${note.noteId}/attributes/${attributeId}`); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @returns - returns true if this attribute has the potential to influence the note in the argument. | ||||
|  *         That can happen in multiple ways: | ||||
| @ -66,6 +88,7 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin | ||||
| export default { | ||||
|     addLabel, | ||||
|     setLabel, | ||||
|     setAttribute, | ||||
|     removeAttributeById, | ||||
|     isAffecting | ||||
| }; | ||||
|  | ||||
| @ -1,10 +1,16 @@ | ||||
| import options from "./options.js"; | ||||
| import i18next from "i18next"; | ||||
| import i18nextHttpBackend from "i18next-http-backend"; | ||||
| import server from "./server.js"; | ||||
| import type { Locale } from "../../../services/i18n.js"; | ||||
| 
 | ||||
| let locales: Locale[] | null; | ||||
| 
 | ||||
| export async function initLocale() { | ||||
|     const locale = (options.get("locale") as string) || "en"; | ||||
| 
 | ||||
|     locales = await server.get<Locale[]>("options/locales"); | ||||
| 
 | ||||
|     await i18next.use(i18nextHttpBackend).init({ | ||||
|         lng: locale, | ||||
|         fallbackLng: "en", | ||||
| @ -15,5 +21,24 @@ export async function initLocale() { | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function getAvailableLocales() { | ||||
|     if (!locales) { | ||||
|         throw new Error("Tried to load list of locales, but localization is not yet initialized.") | ||||
|     } | ||||
| 
 | ||||
|     return locales; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Finds the given locale by ID. | ||||
|  * | ||||
|  * @param localeId the locale ID to search for. | ||||
|  * @returns the corresponding {@link Locale} or `null` if it was not found. | ||||
|  */ | ||||
| export function getLocaleById(localeId: string | null | undefined) { | ||||
|     if (!localeId) return null; | ||||
|     return locales?.find((l) => l.id === localeId) ?? null; | ||||
| } | ||||
| 
 | ||||
| export const t = i18next.t; | ||||
| export const getCurrentLanguage = () => i18next.language; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import dayjs from "dayjs"; | ||||
| import { Modal } from "bootstrap"; | ||||
| import type { ViewScope } from "./link.js"; | ||||
| 
 | ||||
| function reloadFrontendApp(reason?: string) { | ||||
|     if (reason) { | ||||
| @ -388,6 +389,10 @@ function initHelpDropdown($el: JQuery<HTMLElement>) { | ||||
| const wikiBaseUrl = "https://triliumnext.github.io/Docs/Wiki/"; | ||||
| 
 | ||||
| function openHelp($button: JQuery<HTMLElement>) { | ||||
|     if ($button.length === 0) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const helpPage = $button.attr("data-help-page"); | ||||
| 
 | ||||
|     if (helpPage) { | ||||
| @ -397,12 +402,44 @@ function openHelp($button: JQuery<HTMLElement>) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function openInAppHelp($button: JQuery<HTMLElement>) { | ||||
|     if ($button.length === 0) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const inAppHelpPage = $button.attr("data-in-app-help"); | ||||
|     if (inAppHelpPage) { | ||||
|         // Dynamic import to avoid import issues in tests.
 | ||||
|         const appContext = (await import("../components/app_context.js")).default; | ||||
|         const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); | ||||
|         const targetNote = `_help_${inAppHelpPage}`; | ||||
|         const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help"); | ||||
|         const viewScope: ViewScope = { | ||||
|             viewMode: "contextual-help", | ||||
|         }; | ||||
|         if (!helpSubcontext) { | ||||
|             // The help is not already open, open a new split with it.
 | ||||
|             const { ntxId } = subContexts[subContexts.length - 1]; | ||||
|             appContext.triggerCommand("openNewNoteSplit", { | ||||
|                 ntxId, | ||||
|                 notePath: targetNote, | ||||
|                 hoistedNoteId: "_help", | ||||
|                 viewScope | ||||
|             }) | ||||
|         } else { | ||||
|             // There is already a help window open, make sure it opens on the right note.
 | ||||
|             helpSubcontext.setNote(targetNote, { viewScope }); | ||||
|         } | ||||
|         return; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) { | ||||
|     // for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button)
 | ||||
|     // so we do it manually
 | ||||
|     $el.on("click", (e) => { | ||||
|         const $helpButton = $(e.target).closest("[data-help-page]"); | ||||
|         openHelp($helpButton); | ||||
|         openHelp($(e.target).closest("[data-help-page]")); | ||||
|         openInAppHelp($(e.target).closest("[data-in-app-help]")); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										17
									
								
								src/public/app/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -5,6 +5,7 @@ import utils from "./services/utils.ts"; | ||||
| import appContext from "./components/app_context.ts"; | ||||
| import server from "./services/server.ts"; | ||||
| import library_loader, { Library } from "./services/library_loader.ts"; | ||||
| import type { init } from "i18next"; | ||||
| 
 | ||||
| interface ElectronProcess { | ||||
|     type: string; | ||||
| @ -139,10 +140,26 @@ declare global { | ||||
|     } | ||||
|     interface MermaidLoader { | ||||
| 
 | ||||
|     } | ||||
|     interface MermaidChartConfig { | ||||
|         useMaxWidth: boolean; | ||||
|     } | ||||
|     interface MermaidConfig { | ||||
|         theme: string; | ||||
|         securityLevel: "antiscript", | ||||
|         flow: MermaidChartConfig; | ||||
|         sequence: MermaidChartConfig; | ||||
|         gantt: MermaidChartConfig; | ||||
|         class: MermaidChartConfig; | ||||
|         state: MermaidChartConfig; | ||||
|         pie: MermaidChartConfig; | ||||
|         journey: MermaidChartConfig; | ||||
|         git: MermaidChartConfig; | ||||
|     } | ||||
|     var mermaid: { | ||||
|         mermaidAPI: MermaidApi; | ||||
|         registerLayoutLoaders(loader: MermaidLoader); | ||||
|         init(config: MermaidConfig, el: HTMLElement | JQuery<HTMLElement>); | ||||
|         parse(content: string, opts: { | ||||
|             suppressErrors: true | ||||
|         }): Promise<{ | ||||
|  | ||||
| @ -10,7 +10,8 @@ const TPL = ` | ||||
|             position: relative; | ||||
|         } | ||||
| 
 | ||||
|         .floating-buttons-children,.show-floating-buttons { | ||||
|         .floating-buttons-children, | ||||
|         .show-floating-buttons { | ||||
|             position: absolute; | ||||
|             top: 10px; | ||||
|             right: 10px; | ||||
| @ -19,6 +20,21 @@ const TPL = ` | ||||
|             z-index: 100; | ||||
|         } | ||||
| 
 | ||||
|         .note-split.rtl .floating-buttons-children, | ||||
|         .note-split.rtl .show-floating-buttons { | ||||
|             right: unset; | ||||
|             left: 10px; | ||||
|         } | ||||
| 
 | ||||
|         .note-split.rtl .close-floating-buttons { | ||||
|             order: -1; | ||||
|         } | ||||
| 
 | ||||
|         .note-split.rtl .close-floating-buttons, | ||||
|         .note-split.rtl .show-floating-buttons { | ||||
|             transform: rotate(180deg); | ||||
|         } | ||||
| 
 | ||||
|         .type-canvas .floating-buttons-children { | ||||
|             top: 70px; | ||||
|         } | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import appContext, { type EventData } from "../../components/app_context.js"; | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
| import type { NoteType } from "../../entities/fnote.js"; | ||||
| import { t } from "../../services/i18n.js"; | ||||
| import type { ViewScope } from "../../services/link.js"; | ||||
| @ -39,47 +40,28 @@ const byBookType: Record<ViewTypeOptions, string | null> = { | ||||
| 
 | ||||
| export default class ContextualHelpButton extends NoteContextAwareWidget { | ||||
| 
 | ||||
|     private helpNoteIdToOpen?: string | null; | ||||
| 
 | ||||
|     isEnabled() { | ||||
|         this.helpNoteIdToOpen = null; | ||||
| 
 | ||||
|         if (!super.isEnabled()) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         if (this.note && this.note.type !== "book" && byNoteType[this.note.type]) { | ||||
|             this.helpNoteIdToOpen = byNoteType[this.note.type]; | ||||
|         } else if (this.note && this.note.type === "book") { | ||||
|             this.helpNoteIdToOpen = byBookType[(this.note.getAttributeValue("label", "viewType") as ViewTypeOptions) ?? ""]; | ||||
|         } | ||||
| 
 | ||||
|         return !!this.helpNoteIdToOpen; | ||||
|         return !!ContextualHelpButton.#getUrlToOpen(this.note); | ||||
|     } | ||||
| 
 | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.$widget.on("click", () => { | ||||
|             const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts(); | ||||
|             const targetNote = `_help_${this.helpNoteIdToOpen}`; | ||||
|             const helpSubcontext = subContexts?.find((s) => s.viewScope?.viewMode === "contextual-help"); | ||||
|             const viewScope: ViewScope = { | ||||
|                 viewMode: "contextual-help" | ||||
|             }; | ||||
|             if (!helpSubcontext) { | ||||
|                 // The help is not already open, open a new split with it.
 | ||||
|                 const { ntxId } = subContexts?.[subContexts.length - 1] ?? {}; | ||||
|                 this.triggerCommand("openNewNoteSplit", { | ||||
|                     ntxId, | ||||
|                     notePath: targetNote, | ||||
|                     hoistedNoteId: "_help", | ||||
|                     viewScope | ||||
|                 }); | ||||
|             } else { | ||||
|                 // There is already a help window open, make sure it opens on the right note.
 | ||||
|                 helpSubcontext.setNote(targetNote, { viewScope }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     static #getUrlToOpen(note: FNote | null | undefined) { | ||||
|         if (note && note.type !== "book" && byNoteType[note.type]) { | ||||
|             return byNoteType[note.type]; | ||||
|         } else if (note && note.type === "book") { | ||||
|             return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""] | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async refreshWithNote(note: FNote | null | undefined): Promise<void> { | ||||
|         this.$widget.attr("data-in-app-help", ContextualHelpButton.#getUrlToOpen(this.note) ?? ""); | ||||
|     } | ||||
| 
 | ||||
|     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|  | ||||
| @ -143,7 +143,7 @@ export default class MermaidWidget extends NoteContextAwareWidget { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function getMermaidConfig() { | ||||
| export function getMermaidConfig(): MermaidConfig { | ||||
|     const documentStyle = window.getComputedStyle(document.documentElement); | ||||
|     const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme"); | ||||
| 
 | ||||
|  | ||||
| @ -157,6 +157,9 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|             typeWidget.spacedUpdate = this.spacedUpdate; | ||||
|             typeWidget.setParent(this); | ||||
| 
 | ||||
|             if (this.noteContext) { | ||||
|                 typeWidget.setNoteContextEvent({ noteContext: this.noteContext }); | ||||
|             } | ||||
|             const $renderedWidget = typeWidget.render(); | ||||
|             keyboardActionsService.updateDisplayedShortcuts($renderedWidget); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										169
									
								
								src/public/app/widgets/note_language.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,169 @@ | ||||
| import { Dropdown } from "bootstrap"; | ||||
| import NoteContextAwareWidget from "./note_context_aware_widget.js"; | ||||
| import { getAvailableLocales, getLocaleById } from "../services/i18n.js"; | ||||
| import { t } from "i18next"; | ||||
| import type { EventData } from "../components/app_context.js"; | ||||
| import type FNote from "../entities/fnote.js"; | ||||
| import attributes from "../services/attributes.js"; | ||||
| import type { Locale } from "../../../services/i18n.js"; | ||||
| import options from "../services/options.js"; | ||||
| import appContext from "../components/app_context.js"; | ||||
| 
 | ||||
| const TPL = `\ | ||||
| <div class="dropdown note-language-widget"> | ||||
|     <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-language-button"> | ||||
|         <span class="note-language-desc"></span> | ||||
|         <span class="caret"></span> | ||||
|     </button> | ||||
|     <div class="note-language-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div> | ||||
|     <button class="language-help-button icon-action bx bx-help-circle" type="button" data-in-app-help="B0lcI9xz1r8K" title="${t("open-help-page")}"></button> | ||||
| 
 | ||||
|     <style> | ||||
|         .note-language-widget { | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|         } | ||||
| 
 | ||||
|         .language-help-button { | ||||
|             margin-left: 4px; | ||||
|         } | ||||
| 
 | ||||
|         .note-language-dropdown [dir=rtl] { | ||||
|             text-align: right; | ||||
|         } | ||||
| 
 | ||||
|         .dropdown-item.rtl > .check { | ||||
|             order: 1; | ||||
|         } | ||||
|     </style> | ||||
| </div> | ||||
| `;
 | ||||
| 
 | ||||
| const DEFAULT_LOCALE: Locale = { | ||||
|     id: "", | ||||
|     name: t("note_language.not_set") | ||||
| }; | ||||
| 
 | ||||
| export default class NoteLanguageWidget extends NoteContextAwareWidget { | ||||
| 
 | ||||
|     private dropdown!: Dropdown; | ||||
|     private $noteLanguageDropdown!: JQuery<HTMLElement>; | ||||
|     private $noteLanguageDesc!: JQuery<HTMLElement>; | ||||
|     private locales: (Locale | "---")[]; | ||||
|     private currentLanguageId?: string; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.locales = NoteLanguageWidget.#buildLocales(); | ||||
|     } | ||||
| 
 | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]); | ||||
|         this.$widget.on("show.bs.dropdown", () => this.renderDropdown()); | ||||
| 
 | ||||
|         this.$noteLanguageDropdown = this.$widget.find(".note-language-dropdown") | ||||
|         this.$noteLanguageDesc = this.$widget.find(".note-language-desc"); | ||||
|     } | ||||
| 
 | ||||
|     renderDropdown() { | ||||
|         this.$noteLanguageDropdown.empty(); | ||||
| 
 | ||||
|         if (!this.note) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         for (const locale of this.locales) { | ||||
|             if (typeof locale === "object") { | ||||
|                 const $title = $("<span>").text(locale.name); | ||||
| 
 | ||||
|                 const $link = $('<a class="dropdown-item">') | ||||
|                     .attr("data-language", locale.id) | ||||
|                     .append('<span class="check">✓</span> ') | ||||
|                     .append($title) | ||||
|                     .on("click", () => { | ||||
|                     const languageId = $link.attr("data-language") ?? ""; | ||||
|                     this.save(languageId); | ||||
|                 }); | ||||
| 
 | ||||
|                 if (locale.rtl) { | ||||
|                     $link.attr("dir", "rtl"); | ||||
|                 } | ||||
| 
 | ||||
|                 if (locale.id === this.currentLanguageId) { | ||||
|                     $link.addClass("selected"); | ||||
|                 } | ||||
| 
 | ||||
|                 this.$noteLanguageDropdown.append($link); | ||||
|             } else { | ||||
|                 this.$noteLanguageDropdown.append('<div class="dropdown-divider"></div>'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const $configureLink = $('<a class="dropdown-item">') | ||||
|             .append(`<span>${t("note_language.configure-languages")}</span>`) | ||||
|             .on("click", () => appContext.tabManager.openContextWithNote("_optionsLocalization", { activate: true })); | ||||
|         this.$noteLanguageDropdown.append($configureLink); | ||||
|     } | ||||
| 
 | ||||
|     async save(languageId: string) { | ||||
|         if (!this.note) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         attributes.setAttribute(this.note, "label", "language", languageId); | ||||
|     } | ||||
| 
 | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         const currentLanguageId = note.getLabelValue("language") ?? ""; | ||||
|         const language = getLocaleById(currentLanguageId) ?? DEFAULT_LOCALE; | ||||
|         this.currentLanguageId = currentLanguageId; | ||||
|         this.$noteLanguageDesc.text(language.name); | ||||
|         this.dropdown.hide(); | ||||
|     } | ||||
| 
 | ||||
|     async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         if (loadResults.isOptionReloaded("languages")) { | ||||
|             this.locales = NoteLanguageWidget.#buildLocales(); | ||||
|         } | ||||
| 
 | ||||
|         if (loadResults.getAttributeRows().find((a) => a.noteId === this.noteId && a.name === "language")) { | ||||
|             this.refresh(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static #buildLocales() { | ||||
|         const enabledLanguages = JSON.parse(options.get("languages") ?? "[]") as string[]; | ||||
|         const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id)); | ||||
|         const leftToRightLanguages = filteredLanguages.filter((l) => !l.rtl); | ||||
|         const rightToLeftLanguages = filteredLanguages.filter((l) => l.rtl); | ||||
| 
 | ||||
|         let locales: ("---" | Locale)[] = [ | ||||
|             DEFAULT_LOCALE | ||||
|         ]; | ||||
| 
 | ||||
|         if (leftToRightLanguages.length > 0) { | ||||
|             locales = [ | ||||
|                 ...locales, | ||||
|                 "---", | ||||
|                 ...leftToRightLanguages | ||||
|             ]; | ||||
|         } | ||||
| 
 | ||||
|         if (rightToLeftLanguages.length > 0) { | ||||
|             locales = [ | ||||
|                 ...locales, | ||||
|                 "---", | ||||
|                 ...rightToLeftLanguages | ||||
|             ]; | ||||
|         } | ||||
| 
 | ||||
|         // This will separate the list of languages from the "Configure languages" button.
 | ||||
|         // If there is at least one language.
 | ||||
|         if (locales.length > 2) { | ||||
|             locales.push("---"); | ||||
|         } | ||||
|         return locales; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -89,7 +89,10 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { | ||||
|     } | ||||
| 
 | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext?.viewScope?.viewMode !== "default"; | ||||
|         const isReadOnly = | ||||
|             (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) | ||||
|             || utils.isLaunchBarConfig(note.noteId) | ||||
|             || this.noteContext?.viewScope?.viewMode !== "default"; | ||||
| 
 | ||||
|         this.$noteTitle.val(isReadOnly ? (await this.noteContext?.getNavigationTitle()) || "" : note.title); | ||||
|         this.$noteTitle.prop("readonly", isReadOnly); | ||||
|  | ||||
| @ -5,6 +5,7 @@ import type BasicWidget from "./basic_widget.js"; | ||||
| import type { EventData } from "../components/app_context.js"; | ||||
| import type NoteContext from "../components/note_context.js"; | ||||
| import type FNote from "../entities/fnote.js"; | ||||
| import { getLocaleById } from "../services/i18n.js"; | ||||
| 
 | ||||
| export default class NoteWrapperWidget extends FlexContainer<BasicWidget> { | ||||
| 
 | ||||
| @ -56,6 +57,10 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> { | ||||
|         this.$widget.addClass(utils.getMimeTypeClass(note.mime)); | ||||
| 
 | ||||
|         this.$widget.toggleClass("protected", note.isProtected); | ||||
| 
 | ||||
|         const noteLanguage = note?.getLabelValue("language"); | ||||
|         const locale = getLocaleById(noteLanguage); | ||||
|         this.$widget.toggleClass("rtl", !!locale?.rtl); | ||||
|     } | ||||
| 
 | ||||
|     #isFullWidthNote(note: FNote) { | ||||
| @ -76,7 +81,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> { | ||||
|         const noteId = this.noteContext?.noteId; | ||||
|         if ( | ||||
|             loadResults.isNoteReloaded(noteId) || | ||||
|             loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name === "cssClass" && attributeService.isAffecting(attr, this.noteContext?.note)) | ||||
|             loadResults.getAttributeRows().find((attr) => attr.type === "label" && ["cssClass", "language"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.noteContext?.note)) | ||||
|         ) { | ||||
|             this.refresh(); | ||||
|         } | ||||
|  | ||||
| @ -6,6 +6,8 @@ import BookmarkSwitchWidget from "../bookmark_switch.js"; | ||||
| import SharedSwitchWidget from "../shared_switch.js"; | ||||
| import { t } from "../../services/i18n.js"; | ||||
| import TemplateSwitchWidget from "../template_switch.js"; | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
| import NoteLanguageWidget from "../note_language.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="basic-properties-widget"> | ||||
| @ -16,40 +18,55 @@ const TPL = ` | ||||
|             align-items: baseline; | ||||
|             flex-wrap: wrap; | ||||
|         } | ||||
|          | ||||
|         .basic-properties-widget > * {             | ||||
| 
 | ||||
|         .basic-properties-widget > * { | ||||
|             margin-top: 9px; | ||||
|             margin-bottom: 2px; | ||||
|         } | ||||
|          | ||||
| 
 | ||||
|         .basic-properties-widget > * > :last-child { | ||||
|             margin-right: 30px; | ||||
|         } | ||||
| 
 | ||||
|         .note-type-container, .editability-select-container { | ||||
|         .note-type-container, | ||||
|         .editability-select-container, | ||||
|         .note-language-container { | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|         } | ||||
|     </style> | ||||
|      | ||||
| 
 | ||||
|     <div class="note-type-container"> | ||||
|         <span>${t("basic_properties.note_type")}:</span>   | ||||
|     </div> | ||||
|      | ||||
| 
 | ||||
|     <div class="protected-note-switch-container"></div> | ||||
|      | ||||
| 
 | ||||
|     <div class="editability-select-container"> | ||||
|         <span>${t("basic_properties.editable")}:</span>   | ||||
|     </div> | ||||
|      | ||||
| 
 | ||||
|     <div class="bookmark-switch-container"></div> | ||||
|      | ||||
| 
 | ||||
|     <div class="shared-switch-container"></div> | ||||
| 
 | ||||
|     <div class="template-switch-container"></div> | ||||
| 
 | ||||
|     <div class="note-language-container"> | ||||
|         <span>${t("basic_properties.language")}:</span>   | ||||
|     </div> | ||||
| </div>`;
 | ||||
| 
 | ||||
| export default class BasicPropertiesWidget extends NoteContextAwareWidget { | ||||
| 
 | ||||
|     private noteTypeWidget: NoteTypeWidget; | ||||
|     private protectedNoteSwitchWidget: ProtectedNoteSwitchWidget; | ||||
|     private editabilitySelectWidget: EditabilitySelectWidget; | ||||
|     private bookmarkSwitchWidget: BookmarkSwitchWidget; | ||||
|     private sharedSwitchWidget: SharedSwitchWidget; | ||||
|     private templateSwitchWidget: TemplateSwitchWidget; | ||||
|     private noteLanguageWidget: NoteLanguageWidget; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(); | ||||
| 
 | ||||
| @ -59,8 +76,16 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget { | ||||
|         this.bookmarkSwitchWidget = new BookmarkSwitchWidget().contentSized(); | ||||
|         this.sharedSwitchWidget = new SharedSwitchWidget().contentSized(); | ||||
|         this.templateSwitchWidget = new TemplateSwitchWidget().contentSized(); | ||||
|         this.noteLanguageWidget = new NoteLanguageWidget().contentSized(); | ||||
| 
 | ||||
|         this.child(this.noteTypeWidget, this.protectedNoteSwitchWidget, this.editabilitySelectWidget, this.bookmarkSwitchWidget, this.sharedSwitchWidget, this.templateSwitchWidget); | ||||
|         this.child( | ||||
|             this.noteTypeWidget, | ||||
|             this.protectedNoteSwitchWidget, | ||||
|             this.editabilitySelectWidget, | ||||
|             this.bookmarkSwitchWidget, | ||||
|             this.sharedSwitchWidget, | ||||
|             this.templateSwitchWidget, | ||||
|             this.noteLanguageWidget); | ||||
|     } | ||||
| 
 | ||||
|     get name() { | ||||
| @ -73,7 +98,7 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget { | ||||
| 
 | ||||
|     getTitle() { | ||||
|         return { | ||||
|             show: !this.note.isLaunchBarConfig(), | ||||
|             show: !this.note?.isLaunchBarConfig(), | ||||
|             title: t("basic_properties.basic_properties"), | ||||
|             icon: "bx bx-slider" | ||||
|         }; | ||||
| @ -89,11 +114,16 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget { | ||||
|         this.$widget.find(".bookmark-switch-container").append(this.bookmarkSwitchWidget.render()); | ||||
|         this.$widget.find(".shared-switch-container").append(this.sharedSwitchWidget.render()); | ||||
|         this.$widget.find(".template-switch-container").append(this.templateSwitchWidget.render()); | ||||
|         this.$widget.find(".note-language-container").append(this.noteLanguageWidget.render()); | ||||
|     } | ||||
| 
 | ||||
|     async refreshWithNote(note) { | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         await super.refreshWithNote(note); | ||||
|         if (!this.note) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.$widget.find(".editability-select-container").toggle(this.note && ["text", "code"].includes(this.note.type)); | ||||
|         this.$widget.find(".note-language-container").toggle(this.note && ["text"].includes(this.note.type)); | ||||
|     } | ||||
| } | ||||
| @ -5,6 +5,7 @@ import linkService from "../../services/link.js"; | ||||
| import contentRenderer from "../../services/content_renderer.js"; | ||||
| import utils from "../../services/utils.js"; | ||||
| import options from "../../services/options.js"; | ||||
| import attributes from "../../services/attributes.js"; | ||||
| 
 | ||||
| export default class AbstractTextTypeWidget extends TypeWidget { | ||||
|     doRender() { | ||||
| @ -117,5 +118,16 @@ export default class AbstractTextTypeWidget extends TypeWidget { | ||||
|         if (loadResults.isOptionReloaded("codeBlockWordWrap")) { | ||||
|             this.refreshCodeBlockOptions(); | ||||
|         } | ||||
| 
 | ||||
|         if (loadResults.getAttributeRows().find((attr) => | ||||
|             attr.type === "label" && | ||||
|             attr.name === "language" && | ||||
|             attributes.isAffecting(attr, this.note))) | ||||
|         { | ||||
|             await this.onLanguageChanged(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async onLanguageChanged() { } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -39,6 +39,8 @@ import EditorOptions from "./options/text_notes/editor.js"; | ||||
| import ShareSettingsOptions from "./options/other/share_settings.js"; | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
| import type NoteContextAwareWidget from "../note_context_aware_widget.js"; | ||||
| import { t } from "i18next"; | ||||
| import LanguageOptions from "./options/i18n/language.js"; | ||||
| 
 | ||||
| const TPL = `<div class="note-detail-content-widget note-detail-printable">
 | ||||
|     <style> | ||||
| @ -81,6 +83,7 @@ const CONTENT_WIDGETS: Record<string, (typeof NoteContextAwareWidget)[]> = { | ||||
|         HtmlImportTagsOptions, | ||||
|         ShareSettingsOptions | ||||
|     ], | ||||
|     _optionsLocalization: [ LanguageOptions ], | ||||
|     _optionsAdvanced: [DatabaseIntegrityCheckOptions, DatabaseAnonymizationOptions, AdvancedSyncOptions, VacuumDatabaseOptions], | ||||
|     _backendLog: [BackendLogWidget] | ||||
| }; | ||||
| @ -119,7 +122,7 @@ export default class ContentWidgetTypeWidget extends TypeWidget { | ||||
|                 await widget.refresh(); | ||||
|             } | ||||
|         } else { | ||||
|             this.$content.append(`Unknown widget for "${note.noteId}"`); | ||||
|             this.$content.append(t("content_widget.unknown_widget", { id: note.noteId })); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -18,7 +18,7 @@ const TPL = `<div class="note-detail-doc note-detail-printable"> | ||||
|         } | ||||
| 
 | ||||
|         .note-detail-doc.contextual-help { | ||||
|             padding-bottom: 15vh; | ||||
|             padding-bottom: 0; | ||||
|         } | ||||
| 
 | ||||
|         .note-detail-doc.contextual-help h2, | ||||
| @ -36,7 +36,7 @@ const TPL = `<div class="note-detail-doc note-detail-printable"> | ||||
|         } | ||||
| 
 | ||||
|         img { | ||||
|             max-width: 90vw; | ||||
|             max-width: 100%; | ||||
|             height: auto; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -140,8 +140,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|         const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic"; | ||||
|         const editorClass = isClassicEditor ? CKEditor.DecoupledEditor : CKEditor.BalloonEditor; | ||||
| 
 | ||||
|         const codeBlockLanguages = buildListOfLanguages(); | ||||
| 
 | ||||
|         // CKEditor since version 12 needs the element to be visible before initialization. At the same time,
 | ||||
|         // we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
 | ||||
|         // display of $widget in both branches.
 | ||||
| @ -185,7 +183,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|         this.watchdog.setCreator(async (elementOrData, editorConfig) => { | ||||
|             logInfo("Creating new CKEditor"); | ||||
| 
 | ||||
|             const editor = await editorClass.create(elementOrData, { | ||||
|             const finalConfig = { | ||||
|                 ...editorConfig, | ||||
|                 ...buildConfig(), | ||||
|                 ...buildToolbarConfig(isClassicEditor), | ||||
| @ -195,7 +193,20 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|                     classes: true, | ||||
|                     attributes: true | ||||
|                 } | ||||
|             }); | ||||
|             }; | ||||
| 
 | ||||
|             const contentLanguage = this.note.getLabelValue("language"); | ||||
|             if (contentLanguage) { | ||||
|                 finalConfig.language = { | ||||
|                     ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"), | ||||
|                     content: contentLanguage | ||||
|                 } | ||||
|                 this.contentLanguage = contentLanguage; | ||||
|             } else { | ||||
|                 this.contentLanguage = null; | ||||
|             } | ||||
| 
 | ||||
|             const editor = await editorClass.create(elementOrData, finalConfig); | ||||
| 
 | ||||
|             const notificationsPlugin = editor.plugins.get("Notification"); | ||||
|             notificationsPlugin.on("show:warning", (evt, data) => { | ||||
| @ -242,11 +253,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|             return editor; | ||||
|         }); | ||||
| 
 | ||||
|         await this.createEditor(); | ||||
|     } | ||||
| 
 | ||||
|     async createEditor() { | ||||
|         await this.watchdog.create(this.$editor[0], { | ||||
|             placeholder: t("editable_text.placeholder"), | ||||
|             mention: mentionSetup, | ||||
|             codeBlock: { | ||||
|                 languages: codeBlockLanguages | ||||
|                 languages: buildListOfLanguages() | ||||
|             }, | ||||
|             math: { | ||||
|                 engine: "katex", | ||||
| @ -265,7 +280,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|     async doRefresh(note) { | ||||
|         const blob = await note.getBlob(); | ||||
| 
 | ||||
|         await this.spacedUpdate.allowUpdateWithoutChange(() => this.watchdog.editor.setData(blob.content || "")); | ||||
|         await this.spacedUpdate.allowUpdateWithoutChange(async () => { | ||||
|             const data = blob.content || ""; | ||||
|             const newContentLanguage = this.note.getLabelValue("language"); | ||||
|             if (this.contentLanguage !== newContentLanguage) { | ||||
|                 await this.reinitialize(data); | ||||
|             } else { | ||||
|                 this.watchdog.editor.setData(data); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     getData() { | ||||
| @ -468,4 +491,20 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|     async refreshIncludedNoteEvent({ noteId }) { | ||||
|         this.refreshIncludedNote(this.$editor, noteId); | ||||
|     } | ||||
| 
 | ||||
|     async reinitialize(data) { | ||||
|         if (!this.watchdog) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.watchdog.destroy(); | ||||
|         await this.createEditor(); | ||||
|         this.watchdog.editor.setData(data); | ||||
|     } | ||||
| 
 | ||||
|     async onLanguageChanged() { | ||||
|         const data = this.watchdog.editor.getData(); | ||||
|         await this.reinitialize(data); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import OptionsWidget from "../options_widget.js"; | ||||
| import server from "../../../../services/server.js"; | ||||
| import utils from "../../../../services/utils.js"; | ||||
| import { t } from "../../../../services/i18n.js"; | ||||
| import { getAvailableLocales, t } from "../../../../services/i18n.js"; | ||||
| import type { OptionMap } from "../../../../../../services/options_interface.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| @ -25,12 +25,6 @@ const TPL = ` | ||||
| </div> | ||||
| `;
 | ||||
| 
 | ||||
| // TODO: Deduplicate with server.
 | ||||
| interface Locale { | ||||
|     id: string; | ||||
|     name: string; | ||||
| } | ||||
| 
 | ||||
| export default class LocalizationOptions extends OptionsWidget { | ||||
| 
 | ||||
|     private $localeSelect!: JQuery<HTMLElement>; | ||||
| @ -53,7 +47,7 @@ export default class LocalizationOptions extends OptionsWidget { | ||||
|     } | ||||
| 
 | ||||
|     async optionsLoaded(options: OptionMap) { | ||||
|         const availableLocales = await server.get<Locale[]>("options/locales"); | ||||
|         const availableLocales = getAvailableLocales().filter(l => !l.contentOnly); | ||||
|         this.$localeSelect.empty(); | ||||
| 
 | ||||
|         for (const locale of availableLocales) { | ||||
|  | ||||
							
								
								
									
										63
									
								
								src/public/app/widgets/type_widgets/options/i18n/language.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,63 @@ | ||||
| import OptionsWidget from "../options_widget.js"; | ||||
| import type { OptionMap } from "../../../../../../services/options_interface.js"; | ||||
| import { getAvailableLocales } from "../../../../services/i18n.js"; | ||||
| import { t } from "i18next"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="options-section"> | ||||
|     <h4>${t("content_language.title")}</h4> | ||||
|     <p>${t("content_language.description")}</p> | ||||
| 
 | ||||
|     <ul class="options-languages"> | ||||
|     </ul> | ||||
| 
 | ||||
|     <style> | ||||
|         ul.options-languages { | ||||
|             list-style-type: none; | ||||
|             margin-bottom: 0; | ||||
|             column-width: 400px; | ||||
|         } | ||||
|     </style> | ||||
| </div> | ||||
| `;
 | ||||
| 
 | ||||
| export default class LanguageOptions extends OptionsWidget { | ||||
| 
 | ||||
|     private $languagesContainer!: JQuery<HTMLElement>; | ||||
| 
 | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.$languagesContainer = this.$widget.find(".options-languages"); | ||||
|     } | ||||
| 
 | ||||
|     async save() { | ||||
|         const enabledLanguages: string[] = []; | ||||
| 
 | ||||
|         this.$languagesContainer.find("input:checked").each((i, el) => { | ||||
|             const languageId = $(el).attr("data-language-id"); | ||||
|             if (languageId) { | ||||
|                 enabledLanguages.push(languageId); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         await this.updateOption("languages", JSON.stringify(enabledLanguages)); | ||||
|     } | ||||
| 
 | ||||
|     async optionsLoaded(options: OptionMap) { | ||||
|         const availableLocales = getAvailableLocales(); | ||||
|         const enabledLanguages = (JSON.parse(options.languages) as string[]); | ||||
| 
 | ||||
|         this.$languagesContainer.empty(); | ||||
|         for (const locale of availableLocales) { | ||||
|             const checkbox = $('<input type="checkbox" class="form-check-input">') | ||||
|                 .attr("data-language-id", locale.id) | ||||
|                 .prop("checked", enabledLanguages.includes(locale.id)); | ||||
|             const wrapper = $(`<label class="tn-checkbox">`) | ||||
|                 .append(checkbox) | ||||
|                 .on("change", () => this.save()) | ||||
|                 .append(locale.name); | ||||
|             this.$languagesContainer.append($("<li>").append(wrapper)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -2,6 +2,9 @@ import AbstractTextTypeWidget from "./abstract_text_type_widget.js"; | ||||
| import libraryLoader from "../../services/library_loader.js"; | ||||
| import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; | ||||
| import { getMermaidConfig } from "../mermaid.js"; | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
| import type { EventData } from "../../components/app_context.js"; | ||||
| import { getLocaleById } from "../../services/i18n.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <div class="note-detail-readonly-text note-detail-printable"> | ||||
| @ -29,7 +32,7 @@ const TPL = ` | ||||
|     body.heading-style-underline .note-detail-readonly-text h6 { border-bottom: 1px solid var(--main-border-color); } | ||||
| 
 | ||||
|     .note-detail-readonly-text { | ||||
|         padding-left: 24px; | ||||
|         padding-inline-start: 24px; | ||||
|         padding-top: 10px; | ||||
|         font-family: var(--detail-font-family); | ||||
|         min-height: 50px; | ||||
| @ -71,6 +74,9 @@ const TPL = ` | ||||
| `;
 | ||||
| 
 | ||||
| export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { | ||||
| 
 | ||||
|     private $content!: JQuery<HTMLElement>; | ||||
| 
 | ||||
|     static getType() { | ||||
|         return "readOnlyText"; | ||||
|     } | ||||
| @ -89,21 +95,23 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { | ||||
|         this.$content.html(""); | ||||
|     } | ||||
| 
 | ||||
|     async doRefresh(note) { | ||||
|     async doRefresh(note: FNote) { | ||||
|         // we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
 | ||||
|         // we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
 | ||||
|         // (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
 | ||||
|         await libraryLoader.requireLibrary(libraryLoader.CKEDITOR); | ||||
| 
 | ||||
|         this.onLanguageChanged(); | ||||
| 
 | ||||
|         const blob = await note.getBlob(); | ||||
| 
 | ||||
|         this.$content.html(blob.content); | ||||
|         this.$content.html(blob?.content ?? ""); | ||||
| 
 | ||||
|         this.$content.find("a.reference-link").each(async (_, el) => { | ||||
|         this.$content.find("a.reference-link").each((_, el) => { | ||||
|             this.loadReferenceLinkTitle($(el)); | ||||
|         }); | ||||
| 
 | ||||
|         this.$content.find("section").each(async (_, el) => { | ||||
|         this.$content.find("section").each((_, el) => { | ||||
|             const noteId = $(el).attr("data-note-id"); | ||||
| 
 | ||||
|             this.loadIncludedNote(noteId, $(el)); | ||||
| @ -135,11 +143,11 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { | ||||
|         mermaid.init(getMermaidConfig(), this.$content.find(".mermaid-diagram")); | ||||
|     } | ||||
| 
 | ||||
|     async refreshIncludedNoteEvent({ noteId }) { | ||||
|     async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) { | ||||
|         this.refreshIncludedNote(this.$content, noteId); | ||||
|     } | ||||
| 
 | ||||
|     async executeWithContentElementEvent({ resolve, ntxId }) { | ||||
|     async executeWithContentElementEvent({ resolve, ntxId }: EventData<"executeWithContentElement">) { | ||||
|         if (!this.isNoteContext(ntxId)) { | ||||
|             return; | ||||
|         } | ||||
| @ -148,4 +156,12 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { | ||||
| 
 | ||||
|         resolve(this.$content); | ||||
|     } | ||||
| 
 | ||||
|     async onLanguageChanged(): Promise<void> { | ||||
|         const languageCode = this.note?.getLabelValue("language"); | ||||
|         const correspondingLocale = getLocaleById(languageCode); | ||||
|         const isRtl = correspondingLocale?.rtl; | ||||
|         this.$widget.attr("dir", isRtl ? "rtl" : "ltr"); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -229,8 +229,8 @@ export default class CalendarView extends ViewMode { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CalendarView.#setAttribute(note, "label", "startDate", startDate); | ||||
|         CalendarView.#setAttribute(note, "label", "endDate", endDate); | ||||
|         attributes.setAttribute(note, "label", "startDate", startDate); | ||||
|         attributes.setAttribute(note, "label", "endDate", endDate); | ||||
|     } | ||||
| 
 | ||||
|     onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { | ||||
| @ -435,20 +435,6 @@ export default class CalendarView extends ViewMode { | ||||
|         return [note.title]; | ||||
|     } | ||||
| 
 | ||||
|     static async #setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { | ||||
|         if (value) { | ||||
|             // Create or update the attribute.
 | ||||
|             await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); | ||||
|         } else { | ||||
|             // Remove the attribute if it exists on the server but we don't define a value for it.
 | ||||
|             const attributeId = note.getAttribute(type, name)?.attributeId; | ||||
|             if (attributeId) { | ||||
|                 await server.remove(`notes/${note.noteId}/attributes/${attributeId}`); | ||||
|             } | ||||
|         } | ||||
|         await ws.waitForMaxKnownEntityChangeId(); | ||||
|     } | ||||
| 
 | ||||
|     static #formatDateToLocalISO(date: Date | null | undefined) { | ||||
|         if (!date) { | ||||
|             return undefined; | ||||
|  | ||||
| @ -552,7 +552,8 @@ optgroup { | ||||
| 
 | ||||
| /* Switches */ | ||||
| 
 | ||||
| .switch-widget.component { | ||||
| .switch-widget.component, | ||||
| .note-language-container { | ||||
|     --icon-button-size: 28px; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1025,7 +1025,8 @@ body.desktop .dropdown-submenu .dropdown-menu { | ||||
| 
 | ||||
| .dropdown-item, | ||||
| body.mobile .dropdown-submenu .dropdown-toggle { | ||||
|     padding: 2px 16px 2px 8px !important; | ||||
|     padding: 2px 2px 2px 8px !important; | ||||
|     padding-inline-end: 16px; | ||||
|     /* Note: the right padding should also accommodate the submenu arrow. */ | ||||
|     border-radius: 6px; | ||||
|     cursor: default !important; | ||||
|  | ||||
| @ -744,7 +744,8 @@ | ||||
|   "basic_properties": { | ||||
|     "note_type": "Note type", | ||||
|     "editable": "Editable", | ||||
|     "basic_properties": "Basic Properties" | ||||
|     "basic_properties": "Basic Properties", | ||||
|     "language": "Language" | ||||
|   }, | ||||
|   "book_properties": { | ||||
|     "view_type": "View type", | ||||
| @ -1682,5 +1683,16 @@ | ||||
|       "tomorrow": "Tomorrow", | ||||
|       "yesterday": "Yesterday" | ||||
|     } | ||||
|   }, | ||||
|   "content_widget": { | ||||
|     "unknown_widget": "Unknown widget for \"{{id}}\"." | ||||
|   }, | ||||
|   "note_language": { | ||||
|     "not_set": "Not set", | ||||
|     "configure-languages": "Configure languages..." | ||||
|   }, | ||||
|   "content_language": { | ||||
|     "title": "Content languages", | ||||
|     "description": "Select one or more languages that should appear in the language selection in the Basic Properties section of a read-only or editable text note. This will allow features such as spell-checking or right-to-left support." | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1685,5 +1685,8 @@ | ||||
|       "tomorrow": "Mâine", | ||||
|       "yesterday": "Ieri" | ||||
|     } | ||||
|   }, | ||||
|   "content_widget": { | ||||
|     "unknown_widget": "Nu s-a putut găsi widget-ul corespunzător pentru „{{id}}”." | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -71,6 +71,7 @@ const ALLOWED_OPTIONS = new Set([ | ||||
|     "editedNotesOpenInRibbon", | ||||
|     "locale", | ||||
|     "firstDayOfWeek", | ||||
|     "languages", | ||||
|     "textNoteEditorType", | ||||
|     "textNoteEditorMultilineToolbar", | ||||
|     "layoutOrientation", | ||||
|  | ||||
| @ -275,6 +275,7 @@ function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenS | ||||
|                     { id: "_optionsBackup", title: t("hidden-subtree.backup-title"), type: "contentWidget", icon: "bx-data" }, | ||||
|                     { id: "_optionsSync", title: t("hidden-subtree.sync-title"), type: "contentWidget", icon: "bx-wifi" }, | ||||
|                     { id: "_optionsOther", title: t("hidden-subtree.other"), type: "contentWidget", icon: "bx-dots-horizontal" }, | ||||
|                     { id: "_optionsLocalization", title: t("hidden-subtree.localization"), type: "contentWidget", icon: "bx-world" }, | ||||
|                     { id: "_optionsAdvanced", title: t("hidden-subtree.advanced-title"), type: "contentWidget" } | ||||
|                 ] | ||||
|             }, | ||||
|  | ||||
| @ -7,6 +7,10 @@ function checkTranslations(translationDir: string, translationFileName: string) | ||||
|     const locales = i18n.getLocales(); | ||||
| 
 | ||||
|     for (const locale of locales) { | ||||
|         if (locale.contentOnly) { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         const translationPath = path.join(translationDir, locale.id, translationFileName); | ||||
|         const translationFile = fs.readFileSync(translationPath, { encoding: "utf-8" }); | ||||
|         expect(() => { | ||||
|  | ||||
| @ -6,6 +6,76 @@ import { join } from "path"; | ||||
| import { getResourceDir } from "./utils.js"; | ||||
| import hidden_subtree from "./hidden_subtree.js"; | ||||
| 
 | ||||
| export interface Locale { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     /** `true` if the language is a right-to-left one, or `false` if it's left-to-right. */ | ||||
|     rtl?: boolean; | ||||
|     /** `true` if the language is not supported by the application as a display language, but it is selectable by the user for the content. */ | ||||
|     contentOnly?: boolean; | ||||
| } | ||||
| 
 | ||||
| const LOCALES: Locale[] = [ | ||||
|     { | ||||
|         id: "en", | ||||
|         name: "English" | ||||
|     }, | ||||
|     { | ||||
|         id: "de", | ||||
|         name: "Deutsch" | ||||
|     }, | ||||
|     { | ||||
|         id: "es", | ||||
|         name: "Español" | ||||
|     }, | ||||
|     { | ||||
|         id: "fr", | ||||
|         name: "Français" | ||||
|     }, | ||||
|     { | ||||
|         id: "cn", | ||||
|         name: "简体中文" | ||||
|     }, | ||||
|     { | ||||
|         id: "tw", | ||||
|         name: "繁體中文" | ||||
|     }, | ||||
|     { | ||||
|         id: "ro", | ||||
|         name: "Română" | ||||
|     }, | ||||
| 
 | ||||
|     /* | ||||
|      * Right to left languages | ||||
|      * | ||||
|      * Currently they are only for setting the language of text notes. | ||||
|      */ | ||||
|     { // Arabic
 | ||||
|         id: "ar", | ||||
|         name: "اَلْعَرَبِيَّةُ", | ||||
|         rtl: true, | ||||
|         contentOnly: true | ||||
|     }, | ||||
|     { // Hebrew
 | ||||
|         id: "he", | ||||
|         name: "עברית", | ||||
|         rtl: true, | ||||
|         contentOnly: true | ||||
|     }, | ||||
|     { // Kurdish
 | ||||
|         id: "ku", | ||||
|         name: "کوردی", | ||||
|         rtl: true, | ||||
|         contentOnly: true | ||||
|     }, | ||||
|     { // Persian
 | ||||
|         id: "fa", | ||||
|         name: "فارسی", | ||||
|         rtl: true, | ||||
|         contentOnly: true | ||||
|     } | ||||
| ].sort((a, b) => a.name.localeCompare(b.name)); | ||||
| 
 | ||||
| export async function initializeTranslations() { | ||||
|     const resourceDir = getResourceDir(); | ||||
| 
 | ||||
| @ -20,38 +90,8 @@ export async function initializeTranslations() { | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function getLocales() { | ||||
|     // TODO: Currently hardcoded, needs to read the list of available languages.
 | ||||
|     return [ | ||||
|         { | ||||
|             id: "en", | ||||
|             name: "English" | ||||
|         }, | ||||
|         { | ||||
|             id: "de", | ||||
|             name: "Deutsch" | ||||
|         }, | ||||
|         { | ||||
|             id: "es", | ||||
|             name: "Español" | ||||
|         }, | ||||
|         { | ||||
|             id: "fr", | ||||
|             name: "Français" | ||||
|         }, | ||||
|         { | ||||
|             id: "cn", | ||||
|             name: "简体中文" | ||||
|         }, | ||||
|         { | ||||
|             id: "tw", | ||||
|             name: "繁體中文" | ||||
|         }, | ||||
|         { | ||||
|             id: "ro", | ||||
|             name: "Română" | ||||
|         } | ||||
|     ]; | ||||
| export function getLocales(): Locale[] { | ||||
|     return LOCALES; | ||||
| } | ||||
| 
 | ||||
| function getCurrentLanguage() { | ||||
|  | ||||
| @ -134,6 +134,7 @@ const defaultOptions: DefaultOption[] = [ | ||||
|     // Internationalization
 | ||||
|     { name: "locale", value: "en", isSynced: true }, | ||||
|     { name: "firstDayOfWeek", value: "1", isSynced: true }, | ||||
|     { name: "languages", value: "[]", isSynced: true }, | ||||
| 
 | ||||
|     // Code block configuration
 | ||||
|     { | ||||
|  | ||||
| @ -71,6 +71,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi | ||||
|     eraseUnusedAttachmentsAfterSeconds: number; | ||||
|     eraseUnusedAttachmentsAfterTimeScale: number; | ||||
|     firstDayOfWeek: number; | ||||
|     languages: string; | ||||
| 
 | ||||
|     initialized: boolean; | ||||
|     isPasswordSet: boolean; | ||||
|  | ||||
| @ -244,7 +244,8 @@ | ||||
|     "other": "Other", | ||||
|     "advanced-title": "Advanced", | ||||
|     "visible-launchers-title": "Visible Launchers", | ||||
|     "user-guide": "User Guide" | ||||
|     "user-guide": "User Guide", | ||||
|     "localization": "Language & Region" | ||||
|   }, | ||||
|   "notes": { | ||||
|     "new-note": "New note", | ||||
|  | ||||
 Elian Doran
						Elian Doran