| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | import dayjs from "dayjs"; | 
					
						
							| 
									
										
										
										
											2025-03-05 21:38:41 +02:00
										 |  |  | import type { ViewScope } from "./link.js"; | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 18:54:57 +03:00
										 |  |  | const SVG_MIME = "image/svg+xml"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-09 18:22:30 +03:00
										 |  |  | export const isShare = !window.glob; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-14 17:57:08 +03:00
										 |  |  | export function reloadFrontendApp(reason?: string) { | 
					
						
							| 
									
										
										
										
											2021-09-17 22:34:23 +02:00
										 |  |  |     if (reason) { | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |         logInfo(`Frontend app reload: ${reason}`); | 
					
						
							| 
									
										
										
										
											2021-09-17 22:34:23 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-15 21:04:17 +01:00
										 |  |  |     window.location.reload(); | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-14 21:42:48 +03:00
										 |  |  | export function restartDesktopApp() { | 
					
						
							| 
									
										
										
										
											2025-03-25 20:15:39 +02:00
										 |  |  |     if (!isElectron()) { | 
					
						
							|  |  |  |         reloadFrontendApp(); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const app = dynamicRequire("@electron/remote").app; | 
					
						
							|  |  |  |     app.relaunch(); | 
					
						
							|  |  |  |     app.exit(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-01 11:07:47 +02:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Triggers the system tray to update its menu items, i.e. after a change in dynamic content such as bookmarks or recent notes. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * On any other platform than Electron, nothing happens. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2025-02-01 11:04:49 +02:00
										 |  |  | function reloadTray() { | 
					
						
							|  |  |  |     if (!isElectron()) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const { ipcRenderer } = dynamicRequire("electron"); | 
					
						
							|  |  |  |     ipcRenderer.send("reload-tray"); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function parseDate(str: string) { | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     try { | 
					
						
							|  |  |  |         return new Date(Date.parse(str)); | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     } catch (e: any) { | 
					
						
							| 
									
										
										
										
											2023-04-20 00:11:09 +02:00
										 |  |  |         throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`); | 
					
						
							| 
									
										
										
										
											2018-03-24 23:37:55 -04:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-03-24 23:37:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-22 10:56:10 +02:00
										 |  |  | // Source: https://stackoverflow.com/a/30465299/4898894
 | 
					
						
							|  |  |  | function getMonthsInDateRange(startDate: string, endDate: string) { | 
					
						
							| 
									
										
										
										
											2025-03-02 20:47:57 +01:00
										 |  |  |     const start = startDate.split("-"); | 
					
						
							|  |  |  |     const end = endDate.split("-"); | 
					
						
							| 
									
										
										
										
											2025-02-22 10:56:10 +02:00
										 |  |  |     const startYear = parseInt(start[0]); | 
					
						
							|  |  |  |     const endYear = parseInt(end[0]); | 
					
						
							| 
									
										
										
										
											2025-05-28 20:42:21 +03:00
										 |  |  |     const dates: string[] = []; | 
					
						
							| 
									
										
										
										
											2025-02-22 10:56:10 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for (let i = startYear; i <= endYear; i++) { | 
					
						
							|  |  |  |         const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1; | 
					
						
							| 
									
										
										
										
											2025-03-02 20:47:57 +01:00
										 |  |  |         const startMon = i === startYear ? parseInt(start[1]) - 1 : 0; | 
					
						
							| 
									
										
										
										
											2025-02-22 10:56:10 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-02 20:47:57 +01:00
										 |  |  |         for (let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j + 1) { | 
					
						
							|  |  |  |             const month = j + 1; | 
					
						
							|  |  |  |             const displayMonth = month < 10 ? "0" + month : month; | 
					
						
							|  |  |  |             dates.push([i, displayMonth].join("-")); | 
					
						
							| 
									
										
										
										
											2025-02-22 10:56:10 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return dates; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function padNum(num: number) { | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     return `${num <= 9 ? "0" : ""}${num}`; | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-03-24 23:37:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function formatTime(date: Date) { | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`; | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-03-24 23:37:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function formatTimeWithSeconds(date: Date) { | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`; | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-12-23 11:02:38 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function formatTimeInterval(ms: number) { | 
					
						
							| 
									
										
										
										
											2023-04-21 00:19:17 +02:00
										 |  |  |     const seconds = Math.round(ms / 1000); | 
					
						
							|  |  |  |     const minutes = Math.floor(seconds / 60); | 
					
						
							|  |  |  |     const hours = Math.floor(minutes / 60); | 
					
						
							|  |  |  |     const days = Math.floor(hours / 24); | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? "s" : ""}`; | 
					
						
							| 
									
										
										
										
											2025-05-28 20:42:21 +03:00
										 |  |  |     const segments: string[] = []; | 
					
						
							| 
									
										
										
										
											2023-04-21 00:19:17 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (days > 0) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         segments.push(plural(days, "day")); | 
					
						
							| 
									
										
										
										
											2023-04-21 00:19:17 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (days < 2) { | 
					
						
							|  |  |  |         if (hours % 24 > 0) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |             segments.push(plural(hours % 24, "hour")); | 
					
						
							| 
									
										
										
										
											2023-04-21 00:19:17 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (hours < 4) { | 
					
						
							|  |  |  |             if (minutes % 60 > 0) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |                 segments.push(plural(minutes % 60, "minute")); | 
					
						
							| 
									
										
										
										
											2023-04-21 00:19:17 +02:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (minutes < 5) { | 
					
						
							|  |  |  |                 if (seconds % 60 > 0) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |                     segments.push(plural(seconds % 60, "second")); | 
					
						
							| 
									
										
										
										
											2023-04-21 00:19:17 +02:00
										 |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return segments.join(", "); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | /** this is producing local time! **/ | 
					
						
							|  |  |  | function formatDate(date: Date) { | 
					
						
							| 
									
										
										
										
											2023-04-08 21:07:48 +08:00
										 |  |  |     //    return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
 | 
					
						
							| 
									
										
										
										
											2018-08-15 08:48:16 +02:00
										 |  |  |     // instead of european format we'll just use ISO as that's pretty unambiguous
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return formatDateISO(date); | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-12-23 12:19:15 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | /** this is producing local time! **/ | 
					
						
							|  |  |  | function formatDateISO(date: Date) { | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`; | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-12-23 12:19:15 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-18 14:19:38 +03:00
										 |  |  | export function formatDateTime(date: Date, userSuppliedFormat?: string): string { | 
					
						
							| 
									
										
										
										
											2025-06-01 15:27:50 +08:00
										 |  |  |     if (userSuppliedFormat?.trim()) { | 
					
						
							|  |  |  |         return dayjs(date).format(userSuppliedFormat); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |         return `${formatDate(date)} ${formatTime(date)}`; | 
					
						
							| 
									
										
										
										
											2025-05-18 03:14:18 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-12-28 19:00:31 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-26 14:26:57 +02:00
										 |  |  | function localNowDateTime() { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZZ"); | 
					
						
							| 
									
										
										
										
											2020-04-26 14:26:57 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | function now() { | 
					
						
							|  |  |  |     return formatTimeWithSeconds(new Date()); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2018-01-22 23:18:08 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-16 21:41:15 +03:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Returns `true` if the client is currently running under Electron, or `false` if running in a web browser. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2025-08-14 21:42:48 +03:00
										 |  |  | export function isElectron() { | 
					
						
							| 
									
										
										
										
											2019-02-09 19:25:55 +01:00
										 |  |  |     return !!(window && window.process && window.process.type); | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-01-25 23:49:03 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-02 14:04:53 +01:00
										 |  |  | function isMac() { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return navigator.platform.indexOf("Mac") > -1; | 
					
						
							| 
									
										
										
										
											2018-12-02 14:04:53 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-13 23:26:35 +03:00
										 |  |  | export const hasTouchBar = (isMac() && isElectron()); | 
					
						
							| 
									
										
										
										
											2025-04-13 23:09:14 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-28 20:42:21 +03:00
										 |  |  | function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); | 
					
						
							| 
									
										
										
										
											2022-12-09 16:48:00 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-28 15:44:15 +02:00
										 |  |  | function assertArguments<T>(...args: T[]) { | 
					
						
							| 
									
										
										
										
											2024-07-25 20:55:04 +03:00
										 |  |  |     for (const i in args) { | 
					
						
							|  |  |  |         if (!args[i]) { | 
					
						
							|  |  |  |             console.trace(`Argument idx#${i} should not be falsy: ${args[i]}`); | 
					
						
							| 
									
										
										
										
											2018-03-24 23:37:55 -04:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-03-06 23:04:35 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | const entityMap: Record<string, string> = { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     "&": "&", | 
					
						
							|  |  |  |     "<": "<", | 
					
						
							|  |  |  |     ">": ">", | 
					
						
							|  |  |  |     '"': """, | 
					
						
							|  |  |  |     "'": "'", | 
					
						
							|  |  |  |     "/": "/", | 
					
						
							|  |  |  |     "`": "`", | 
					
						
							|  |  |  |     "=": "=" | 
					
						
							| 
									
										
										
										
											2020-06-24 21:07:55 +02:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function escapeHtml(str: string) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]); | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-03-24 23:37:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-28 21:03:39 +02:00
										 |  |  | export function escapeQuotes(value: string) { | 
					
						
							| 
									
										
										
										
											2025-03-02 20:47:57 +01:00
										 |  |  |     return value.replaceAll('"', """); | 
					
						
							| 
									
										
										
										
											2025-01-28 21:03:39 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function formatSize(size: number) { | 
					
						
							| 
									
										
										
										
											2023-03-24 10:57:32 +01:00
										 |  |  |     size = Math.max(Math.round(size / 1024), 1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (size < 1024) { | 
					
						
							|  |  |  |         return `${size} KiB`; | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2023-03-24 10:57:32 +01:00
										 |  |  |         return `${Math.round(size / 102.4) / 10} MiB`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-19 22:16:03 +02:00
										 |  |  | function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) { | 
					
						
							|  |  |  |     const obj: Record<string, R> = {}; | 
					
						
							| 
									
										
										
										
											2018-03-04 23:28:26 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     for (const item of array) { | 
					
						
							| 
									
										
										
										
											2018-03-25 23:25:17 -04:00
										 |  |  |         const [key, value] = fn(item); | 
					
						
							| 
									
										
										
										
											2018-03-24 23:37:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-25 23:25:17 -04:00
										 |  |  |         obj[key] = value; | 
					
						
							| 
									
										
										
										
											2018-03-04 23:28:26 -05:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     return obj; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2018-03-12 23:27:21 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function randomString(len: number) { | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     let text = ""; | 
					
						
							|  |  |  |     const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | 
					
						
							| 
									
										
										
										
											2018-03-12 23:27:21 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     for (let i = 0; i < len; i++) { | 
					
						
							|  |  |  |         text += possible.charAt(Math.floor(Math.random() * possible.length)); | 
					
						
							| 
									
										
										
										
											2018-03-12 23:27:21 -04:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     return text; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-14 17:57:08 +03:00
										 |  |  | export function isMobile() { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return ( | 
					
						
							|  |  |  |         window.glob?.device === "mobile" || | 
					
						
							| 
									
										
										
										
											2023-05-03 22:49:24 +02:00
										 |  |  |         // window.glob.device is not available in setup
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         (!window.glob?.device && /Mobi/.test(navigator.userAgent)) | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2018-12-24 10:10:36 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-10 18:21:36 +03:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Returns true if the client device is an Apple iOS one (iPad, iPhone, iPod). | 
					
						
							|  |  |  |  * Does not check if the user requested the mobile or desktop layout, use {@link isMobile} for that. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @returns `true` if running under iOS. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export function isIOS() { | 
					
						
							|  |  |  |     return /iPad|iPhone|iPod/.test(navigator.userAgent); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  | function isDesktop() { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return ( | 
					
						
							|  |  |  |         window.glob?.device === "desktop" || | 
					
						
							| 
									
										
										
										
											2023-05-03 22:49:24 +02:00
										 |  |  |         // window.glob.device is not available in setup
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         (!window.glob?.device && !/Mobi/.test(navigator.userAgent)) | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2018-12-24 10:10:36 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | /** | 
					
						
							|  |  |  |  * the cookie code below works for simple use cases only - ASCII only | 
					
						
							|  |  |  |  * not setting a path so that cookies do not leak into other websites if multiplexed with reverse proxy | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function setCookie(name: string, value: string) { | 
					
						
							| 
									
										
										
										
											2018-12-29 00:09:16 +01:00
										 |  |  |     const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000); | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     const expires = `; expires=${date.toUTCString()}`; | 
					
						
							| 
									
										
										
										
											2018-12-29 00:09:16 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     document.cookie = `${name}=${value || ""}${expires};`; | 
					
						
							| 
									
										
										
										
											2019-03-13 21:53:09 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function getNoteTypeClass(type: string) { | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     return `type-${type}`; | 
					
						
							| 
									
										
										
										
											2019-01-28 21:42:37 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function getMimeTypeClass(mime: string) { | 
					
						
							| 
									
										
										
										
											2021-02-22 21:59:37 +01:00
										 |  |  |     if (!mime) { | 
					
						
							|  |  |  |         return ""; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     const semicolonIdx = mime.indexOf(";"); | 
					
						
							| 
									
										
										
										
											2019-01-28 21:42:37 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (semicolonIdx !== -1) { | 
					
						
							|  |  |  |         // stripping everything following the semicolon
 | 
					
						
							|  |  |  |         mime = mime.substr(0, semicolonIdx); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 15:19:05 +01:00
										 |  |  |     return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; | 
					
						
							| 
									
										
										
										
											2019-01-28 21:42:37 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function isHtmlEmpty(html: string) { | 
					
						
							| 
									
										
										
										
											2020-03-26 16:59:40 +01:00
										 |  |  |     if (!html) { | 
					
						
							|  |  |  |         return true; | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     } else if (typeof html !== "string") { | 
					
						
							| 
									
										
										
										
											2023-06-14 22:21:22 +02:00
										 |  |  |         logError(`Got object of type '${typeof html}' where string was expected.`); | 
					
						
							|  |  |  |         return false; | 
					
						
							| 
									
										
										
										
											2020-03-26 16:59:40 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-04 13:22:07 +01:00
										 |  |  |     html = html.toLowerCase(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return ( | 
					
						
							|  |  |  |         !html.includes("<img") && | 
					
						
							|  |  |  |         !html.includes("<section") && | 
					
						
							| 
									
										
										
										
											2023-06-14 22:21:22 +02:00
										 |  |  |         // the line below will actually attempt to load images so better to check for images first
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         $("<div>").html(html).text().trim().length === 0 | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2019-10-05 20:27:30 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-08 23:09:57 +01:00
										 |  |  | async function clearBrowserCache() { | 
					
						
							|  |  |  |     if (isElectron()) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         const win = dynamicRequire("@electron/remote").getCurrentWindow(); | 
					
						
							| 
									
										
										
										
											2019-11-08 23:09:57 +01:00
										 |  |  |         await win.webContents.session.clearCache(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-19 11:16:36 +03:00
										 |  |  | function copySelectionToClipboard() { | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  |     const text = window?.getSelection()?.toString(); | 
					
						
							|  |  |  |     if (text && navigator.clipboard) { | 
					
						
							| 
									
										
										
										
											2020-01-19 09:25:35 +01:00
										 |  |  |         navigator.clipboard.writeText(text); | 
					
						
							| 
									
										
										
										
											2020-01-19 11:16:36 +03:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-15 13:52:52 +03:00
										 |  |  | export function dynamicRequire(moduleName: string) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     if (typeof __non_webpack_require__ !== "undefined") { | 
					
						
							| 
									
										
										
										
											2020-04-12 14:22:51 +02:00
										 |  |  |         return __non_webpack_require__(moduleName); | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2025-01-19 12:27:43 +01:00
										 |  |  |         // explicitly pass as string and not as expression to suppress webpack warning
 | 
					
						
							|  |  |  |         // 'Critical dependency: the request of a dependency is an expression'
 | 
					
						
							|  |  |  |         return require(`${moduleName}`); | 
					
						
							| 
									
										
										
										
											2020-04-12 14:22:51 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 20:47:33 +03:00
										 |  |  | function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     if (!promise || !promise.then) { | 
					
						
							|  |  |  |         // it's not actually a promise
 | 
					
						
							| 
									
										
										
										
											2021-02-20 23:17:29 +01:00
										 |  |  |         return promise; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-29 23:34:49 +02:00
										 |  |  |     // better stack trace if created outside of promise
 | 
					
						
							| 
									
										
										
										
											2021-02-20 23:17:29 +01:00
										 |  |  |     const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`); | 
					
						
							| 
									
										
										
										
											2020-07-29 23:34:49 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 19:21:24 +03:00
										 |  |  |     return new Promise<T>((res, rej) => { | 
					
						
							| 
									
										
										
										
											2020-06-11 00:13:56 +02:00
										 |  |  |         let resolved = false; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         promise.then((result) => { | 
					
						
							| 
									
										
										
										
											2020-06-11 00:13:56 +02:00
										 |  |  |             resolved = true; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 22:34:15 +02:00
										 |  |  |             res(result); | 
					
						
							| 
									
										
										
										
											2020-06-11 00:13:56 +02:00
										 |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         setTimeout(() => { | 
					
						
							|  |  |  |             if (!resolved) { | 
					
						
							| 
									
										
										
										
											2020-07-29 23:34:49 +02:00
										 |  |  |                 rej(error); | 
					
						
							| 
									
										
										
										
											2020-06-11 00:13:56 +02:00
										 |  |  |             } | 
					
						
							|  |  |  |         }, limitMs); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function initHelpDropdown($el: JQuery<HTMLElement>) { | 
					
						
							| 
									
										
										
										
											2021-01-26 14:44:53 +01:00
										 |  |  |     // stop inside clicks from closing the menu
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     const $dropdownMenu = $el.find(".help-dropdown .dropdown-menu"); | 
					
						
							|  |  |  |     $dropdownMenu.on("click", (e) => e.stopPropagation()); | 
					
						
							| 
									
										
										
										
											2021-01-26 14:44:53 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-15 21:04:17 +01:00
										 |  |  |     // previous propagation stop will also block help buttons from being opened, so we need to re-init for this element
 | 
					
						
							| 
									
										
										
										
											2021-01-26 14:44:53 +01:00
										 |  |  |     initHelpButtons($dropdownMenu); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-10 00:26:39 +03:00
										 |  |  | const wikiBaseUrl = "https://triliumnext.github.io/Docs/Wiki/"; | 
					
						
							| 
									
										
										
										
											2021-01-26 14:44:53 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function openHelp($button: JQuery<HTMLElement>) { | 
					
						
							| 
									
										
										
										
											2025-03-05 21:38:41 +02:00
										 |  |  |     if ($button.length === 0) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-14 21:59:43 +02:00
										 |  |  |     const helpPage = $button.attr("data-help-page"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (helpPage) { | 
					
						
							|  |  |  |         const url = wikiBaseUrl + helpPage; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         window.open(url, "_blank"); | 
					
						
							| 
									
										
										
										
											2023-07-14 21:59:43 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-12-20 17:30:47 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-05 22:11:03 +02:00
										 |  |  | async function openInAppHelp($button: JQuery<HTMLElement>) { | 
					
						
							| 
									
										
										
										
											2025-03-05 21:38:41 +02:00
										 |  |  |     if ($button.length === 0) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const inAppHelpPage = $button.attr("data-in-app-help"); | 
					
						
							|  |  |  |     if (inAppHelpPage) { | 
					
						
							| 
									
										
										
										
											2025-08-15 11:21:19 +03:00
										 |  |  |         openInAppHelpFromUrl(inAppHelpPage); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export async function openInAppHelpFromUrl(inAppHelpPage: string) { | 
					
						
							|  |  |  |     // Dynamic import to avoid import issues in tests.
 | 
					
						
							|  |  |  |     const appContext = (await import("../components/app_context.js")).default; | 
					
						
							|  |  |  |     const activeContext = appContext.tabManager.getActiveContext(); | 
					
						
							|  |  |  |     if (!activeContext) { | 
					
						
							| 
									
										
										
										
											2025-03-05 21:38:41 +02:00
										 |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-15 11:21:19 +03:00
										 |  |  |     const subContexts = activeContext.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 }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-03-05 21:38:41 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-21 17:47:09 +02:00
										 |  |  | function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) { | 
					
						
							| 
									
										
										
										
											2023-06-29 23:32:19 +02:00
										 |  |  |     // for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button)
 | 
					
						
							| 
									
										
										
										
											2022-12-08 15:18:41 +01:00
										 |  |  |     // so we do it manually
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     $el.on("click", (e) => { | 
					
						
							| 
									
										
										
										
											2025-03-05 21:38:41 +02:00
										 |  |  |         openHelp($(e.target).closest("[data-help-page]")); | 
					
						
							|  |  |  |         openInAppHelp($(e.target).closest("[data-in-app-help]")); | 
					
						
							| 
									
										
										
										
											2022-12-08 15:18:41 +01:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2021-01-26 14:44:53 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function filterAttributeName(name: string) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return name.replace(/[^\p{L}\p{N}_:]/gu, ""); | 
					
						
							| 
									
										
										
										
											2021-02-17 23:22:14 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function isValidAttributeName(name: string) { | 
					
						
							| 
									
										
										
										
											2021-02-17 23:22:14 +01:00
										 |  |  |     return ATTR_NAME_MATCHER.test(name); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function sleep(time_ms: number) { | 
					
						
							| 
									
										
										
										
											2022-05-09 16:50:06 +02:00
										 |  |  |     return new Promise((resolve) => { | 
					
						
							|  |  |  |         setTimeout(resolve, time_ms); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2022-05-13 23:20:56 +02:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2022-05-09 16:50:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function escapeRegExp(str: string) { | 
					
						
							| 
									
										
										
										
											2022-05-25 23:38:06 +02:00
										 |  |  |     return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-23 15:16:41 +02:00
										 |  |  | function areObjectsEqual(...args: unknown[]) { | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  |     let i; | 
					
						
							|  |  |  |     let l; | 
					
						
							|  |  |  |     let leftChain: Object[]; | 
					
						
							|  |  |  |     let rightChain: Object[]; | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     function compare2Objects(x: unknown, y: unknown) { | 
					
						
							| 
									
										
										
										
											2023-05-05 23:17:23 +02:00
										 |  |  |         let p; | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // remember that NaN === NaN returns false
 | 
					
						
							|  |  |  |         // and isNaN(undefined) returns true
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         if (typeof x === "number" && typeof y === "number" && isNaN(x) && isNaN(y)) { | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |             return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Compare primitives and functions.
 | 
					
						
							|  |  |  |         // Check if both arguments link to the same object.
 | 
					
						
							|  |  |  |         // Especially useful on the step where we compare prototypes
 | 
					
						
							|  |  |  |         if (x === y) { | 
					
						
							|  |  |  |             return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Works in case when functions are created in constructor.
 | 
					
						
							|  |  |  |         // Comparing dates is a common scenario. Another built-ins?
 | 
					
						
							|  |  |  |         // We can even handle functions passed across iframes
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         if ( | 
					
						
							|  |  |  |             (typeof x === "function" && typeof y === "function") || | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |             (x instanceof Date && y instanceof Date) || | 
					
						
							|  |  |  |             (x instanceof RegExp && y instanceof RegExp) || | 
					
						
							|  |  |  |             (x instanceof String && y instanceof String) || | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |             (x instanceof Number && y instanceof Number) | 
					
						
							|  |  |  |         ) { | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |             return x.toString() === y.toString(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-30 11:18:34 +02:00
										 |  |  |         // At last, checking prototypes as good as we can
 | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |         if (!(x instanceof Object && y instanceof Object)) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (x.constructor !== y.constructor) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  |         if ((x as any).prototype !== (y as any).prototype) { | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Check for infinitive linking loops
 | 
					
						
							|  |  |  |         if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Quick checking of one object being a subset of another.
 | 
					
						
							|  |  |  |         // todo: cache the structure of arguments[0] for performance
 | 
					
						
							|  |  |  |         for (p in y) { | 
					
						
							|  |  |  |             if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { | 
					
						
							|  |  |  |                 return false; | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |             } else if (typeof (y as any)[p] !== typeof (x as any)[p]) { | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |                 return false; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for (p in x) { | 
					
						
							|  |  |  |             if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { | 
					
						
							|  |  |  |                 return false; | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |             } else if (typeof (y as any)[p] !== typeof (x as any)[p]) { | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |                 return false; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |             switch (typeof (x as any)[p]) { | 
					
						
							|  |  |  |                 case "object": | 
					
						
							|  |  |  |                 case "function": | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |                     leftChain.push(x); | 
					
						
							|  |  |  |                     rightChain.push(y); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  |                     if (!compare2Objects((x as any)[p], (y as any)[p])) { | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |                         return false; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     leftChain.pop(); | 
					
						
							|  |  |  |                     rightChain.pop(); | 
					
						
							|  |  |  |                     break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 default: | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  |                     if ((x as any)[p] !== (y as any)[p]) { | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |                         return false; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     break; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (arguments.length < 1) { | 
					
						
							|  |  |  |         return true; //Die silently? Don't know how to handle such case, please help...
 | 
					
						
							|  |  |  |         // throw "Need two or more arguments to compare";
 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for (i = 1, l = arguments.length; i < l; i++) { | 
					
						
							|  |  |  |         leftChain = []; //Todo: this can be cached
 | 
					
						
							|  |  |  |         rightChain = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!compare2Objects(arguments[0], arguments[i])) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | function copyHtmlToClipboard(content: string) { | 
					
						
							|  |  |  |     function listener(e: ClipboardEvent) { | 
					
						
							|  |  |  |         if (e.clipboardData) { | 
					
						
							|  |  |  |             e.clipboardData.setData("text/html", content); | 
					
						
							|  |  |  |             e.clipboardData.setData("text/plain", content); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-03-27 20:42:36 +01:00
										 |  |  |         e.preventDefault(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     document.addEventListener("copy", listener); | 
					
						
							|  |  |  |     document.execCommand("copy"); | 
					
						
							|  |  |  |     document.removeEventListener("copy", listener); | 
					
						
							| 
									
										
										
										
											2023-05-29 00:19:54 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-25 00:18:57 +03:00
										 |  |  | // TODO: Set to FNote once the file is ported.
 | 
					
						
							|  |  |  | function createImageSrcUrl(note: { noteId: string; title: string }) { | 
					
						
							| 
									
										
										
										
											2023-10-19 09:20:23 +02:00
										 |  |  |     return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-01 23:20:41 +03:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Given a string representation of an SVG, triggers a download of the file on the client device. | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  |  * | 
					
						
							| 
									
										
										
										
											2024-10-26 10:29:15 +03:00
										 |  |  |  * @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it. | 
					
						
							|  |  |  |  * @param svgContent the content of the SVG file download. | 
					
						
							| 
									
										
										
										
											2024-09-01 23:20:41 +03:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-10-26 10:29:15 +03:00
										 |  |  | function downloadSvg(nameWithoutExtension: string, svgContent: string) { | 
					
						
							| 
									
										
										
										
											2024-09-01 23:20:41 +03:00
										 |  |  |     const filename = `${nameWithoutExtension}.svg`; | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  |     const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`; | 
					
						
							|  |  |  |     triggerDownload(filename, dataUrl); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Downloads the given data URL on the client device, with a custom file name. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @param fileName the name to give the downloaded file. | 
					
						
							|  |  |  |  * @param dataUrl the data URI to download. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function triggerDownload(fileName: string, dataUrl: string) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     const element = document.createElement("a"); | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  |     element.setAttribute("href", dataUrl); | 
					
						
							|  |  |  |     element.setAttribute("download", fileName); | 
					
						
							| 
									
										
										
										
											2024-09-01 23:20:41 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     element.style.display = "none"; | 
					
						
							| 
									
										
										
										
											2024-09-01 23:20:41 +03:00
										 |  |  |     document.body.appendChild(element); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     element.click(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     document.body.removeChild(element); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Note that the SVG must specify its width and height as attributes in order for it to be rendered. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it. | 
					
						
							|  |  |  |  * @param svgContent the content of the SVG file download. | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  |  * @returns a promise which resolves if the operation was successful, or rejects if it failed (permissions issue or some other issue). | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  |  */ | 
					
						
							|  |  |  | function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) { | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  |     return new Promise<void>((resolve, reject) => { | 
					
						
							|  |  |  |         // First, we need to determine the width and the height from the input SVG.
 | 
					
						
							| 
									
										
										
										
											2025-03-31 18:54:57 +03:00
										 |  |  |         const result = getSizeFromSvg(svgContent); | 
					
						
							|  |  |  |         if (!result) { | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  |             reject(); | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Convert the image to a blob.
 | 
					
						
							| 
									
										
										
										
											2025-03-31 18:54:57 +03:00
										 |  |  |         const { width, height } = result; | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // Create an image element and load the SVG.
 | 
					
						
							|  |  |  |         const imageEl = new Image(); | 
					
						
							| 
									
										
										
										
											2025-03-31 18:54:57 +03:00
										 |  |  |         imageEl.width = width; | 
					
						
							|  |  |  |         imageEl.height = height; | 
					
						
							| 
									
										
										
										
											2025-03-31 21:18:40 +03:00
										 |  |  |         imageEl.crossOrigin = "anonymous"; | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  |         imageEl.onload = () => { | 
					
						
							|  |  |  |             try { | 
					
						
							|  |  |  |                 // Draw the image with a canvas.
 | 
					
						
							|  |  |  |                 const canvasEl = document.createElement("canvas"); | 
					
						
							|  |  |  |                 canvasEl.width = imageEl.width; | 
					
						
							|  |  |  |                 canvasEl.height = imageEl.height; | 
					
						
							|  |  |  |                 document.body.appendChild(canvasEl); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 const ctx = canvasEl.getContext("2d"); | 
					
						
							|  |  |  |                 if (!ctx) { | 
					
						
							|  |  |  |                     reject(); | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  |                 ctx?.drawImage(imageEl, 0, 0); | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  |                 const imgUri = canvasEl.toDataURL("image/png") | 
					
						
							|  |  |  |                 triggerDownload(`${nameWithoutExtension}.png`, imgUri); | 
					
						
							|  |  |  |                 document.body.removeChild(canvasEl); | 
					
						
							|  |  |  |                 resolve(); | 
					
						
							|  |  |  |             } catch (e) { | 
					
						
							|  |  |  |                 console.warn(e); | 
					
						
							|  |  |  |                 reject(); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2025-03-31 21:18:40 +03:00
										 |  |  |         imageEl.onerror = (e) => reject(e); | 
					
						
							|  |  |  |         imageEl.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`; | 
					
						
							| 
									
										
										
										
											2025-03-31 18:36:57 +03:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 18:54:57 +03:00
										 |  |  | export function getSizeFromSvg(svgContent: string) { | 
					
						
							|  |  |  |     const svgDocument = (new DOMParser()).parseFromString(svgContent, SVG_MIME); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Try to use width & height attributes if available.
 | 
					
						
							|  |  |  |     let width = svgDocument.documentElement?.getAttribute("width"); | 
					
						
							|  |  |  |     let height = svgDocument.documentElement?.getAttribute("height"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // If not, use the viewbox.
 | 
					
						
							|  |  |  |     if (!width || !height) { | 
					
						
							|  |  |  |         const viewBox = svgDocument.documentElement?.getAttribute("viewBox"); | 
					
						
							|  |  |  |         if (viewBox) { | 
					
						
							|  |  |  |             const viewBoxParts = viewBox.split(" "); | 
					
						
							|  |  |  |             width = viewBoxParts[2]; | 
					
						
							|  |  |  |             height = viewBoxParts[3]; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (width && height) { | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             width: parseFloat(width), | 
					
						
							|  |  |  |             height: parseFloat(height) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |         console.warn("SVG export error", svgDocument.documentElement); | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Compares two semantic version strings. | 
					
						
							|  |  |  |  * Returns: | 
					
						
							|  |  |  |  *   1  if v1 is greater than v2 | 
					
						
							|  |  |  |  *   0  if v1 is equal to v2 | 
					
						
							|  |  |  |  *   -1 if v1 is less than v2 | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  |  * | 
					
						
							| 
									
										
										
										
											2024-12-14 09:43:01 +02:00
										 |  |  |  * @param v1 First version string | 
					
						
							|  |  |  |  * @param v2 Second version string | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  |  * @returns | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-12-14 09:43:01 +02:00
										 |  |  | function compareVersions(v1: string, v2: string): number { | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     // Remove 'v' prefix and everything after dash if present
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     v1 = v1.replace(/^v/, "").split("-")[0]; | 
					
						
							|  |  |  |     v2 = v2.replace(/^v/, "").split("-")[0]; | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     const v1parts = v1.split(".").map(Number); | 
					
						
							|  |  |  |     const v2parts = v2.split(".").map(Number); | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     // Pad shorter version with zeros
 | 
					
						
							|  |  |  |     while (v1parts.length < 3) v1parts.push(0); | 
					
						
							|  |  |  |     while (v2parts.length < 3) v2parts.push(0); | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     // Compare major version
 | 
					
						
							|  |  |  |     if (v1parts[0] !== v2parts[0]) { | 
					
						
							|  |  |  |         return v1parts[0] > v2parts[0] ? 1 : -1; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     // Compare minor version
 | 
					
						
							|  |  |  |     if (v1parts[1] !== v2parts[1]) { | 
					
						
							|  |  |  |         return v1parts[1] > v2parts[1] ? 1 : -1; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     // Compare patch version
 | 
					
						
							|  |  |  |     if (v1parts[2] !== v2parts[2]) { | 
					
						
							|  |  |  |         return v1parts[2] > v2parts[2] ? 1 : -1; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-12-23 11:00:10 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     return 0; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-09 22:23:02 +00:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Compares two semantic version strings and returns `true` if the latest version is greater than the current version. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2025-02-08 10:50:55 +02:00
										 |  |  | function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean { | 
					
						
							|  |  |  |     if (!latestVersion) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     return compareVersions(latestVersion, currentVersion) > 0; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 21:52:41 +02:00
										 |  |  | function isLaunchBarConfig(noteId: string) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId); | 
					
						
							| 
									
										
										
										
											2025-01-04 21:52:41 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-18 09:47:18 +03:00
										 |  |  | export function toggleBodyClass(prefix: string, value: string) { | 
					
						
							|  |  |  |     const $body = $("body"); | 
					
						
							|  |  |  |     for (const clazz of Array.from($body[0].classList)) { | 
					
						
							|  |  |  |         // create copy to safely iterate over while removing classes
 | 
					
						
							|  |  |  |         if (clazz.startsWith(prefix)) { | 
					
						
							|  |  |  |             $body.removeClass(clazz); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     $body.addClass(prefix + value); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-16 18:33:39 +03:00
										 |  |  | export default { | 
					
						
							| 
									
										
										
										
											2021-08-24 22:59:51 +02:00
										 |  |  |     reloadFrontendApp, | 
					
						
							| 
									
										
										
										
											2025-03-25 20:15:39 +02:00
										 |  |  |     restartDesktopApp, | 
					
						
							| 
									
										
										
										
											2025-02-01 11:04:49 +02:00
										 |  |  |     reloadTray, | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     parseDate, | 
					
						
							| 
									
										
										
										
											2025-02-22 10:56:10 +02:00
										 |  |  |     getMonthsInDateRange, | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     formatDateISO, | 
					
						
							|  |  |  |     formatDateTime, | 
					
						
							| 
									
										
										
										
											2023-04-21 00:19:17 +02:00
										 |  |  |     formatTimeInterval, | 
					
						
							| 
									
										
										
										
											2023-03-24 10:57:32 +01:00
										 |  |  |     formatSize, | 
					
						
							| 
									
										
										
										
											2020-04-26 14:26:57 +02:00
										 |  |  |     localNowDateTime, | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     now, | 
					
						
							|  |  |  |     isElectron, | 
					
						
							| 
									
										
										
										
											2018-12-02 14:04:53 +01:00
										 |  |  |     isMac, | 
					
						
							| 
									
										
										
										
											2022-12-09 16:48:00 +01:00
										 |  |  |     isCtrlKey, | 
					
						
							| 
									
										
										
										
											2018-03-25 11:09:17 -04:00
										 |  |  |     assertArguments, | 
					
						
							|  |  |  |     escapeHtml, | 
					
						
							|  |  |  |     toObject, | 
					
						
							| 
									
										
										
										
											2018-03-25 19:49:33 -04:00
										 |  |  |     randomString, | 
					
						
							| 
									
										
										
										
											2018-12-24 10:10:36 +01:00
										 |  |  |     isMobile, | 
					
						
							| 
									
										
										
										
											2018-12-29 00:09:16 +01:00
										 |  |  |     isDesktop, | 
					
						
							| 
									
										
										
										
											2019-01-28 21:42:37 +01:00
										 |  |  |     setCookie, | 
					
						
							|  |  |  |     getNoteTypeClass, | 
					
						
							| 
									
										
										
										
											2019-06-10 22:45:03 +02:00
										 |  |  |     getMimeTypeClass, | 
					
						
							| 
									
										
										
										
											2019-11-08 23:09:57 +01:00
										 |  |  |     isHtmlEmpty, | 
					
						
							| 
									
										
										
										
											2019-11-22 20:35:17 +01:00
										 |  |  |     clearBrowserCache, | 
					
						
							| 
									
										
										
										
											2020-02-02 16:28:19 +08:00
										 |  |  |     copySelectionToClipboard, | 
					
						
							| 
									
										
										
										
											2020-06-11 00:13:56 +02:00
										 |  |  |     dynamicRequire, | 
					
						
							| 
									
										
										
										
											2021-01-26 14:44:53 +01:00
										 |  |  |     timeLimit, | 
					
						
							|  |  |  |     initHelpDropdown, | 
					
						
							| 
									
										
										
										
											2021-02-17 23:22:14 +01:00
										 |  |  |     initHelpButtons, | 
					
						
							| 
									
										
										
										
											2021-12-20 17:30:47 +01:00
										 |  |  |     openHelp, | 
					
						
							| 
									
										
										
										
											2021-02-17 23:22:14 +01:00
										 |  |  |     filterAttributeName, | 
					
						
							| 
									
										
										
										
											2022-05-09 16:50:06 +02:00
										 |  |  |     isValidAttributeName, | 
					
						
							|  |  |  |     sleep, | 
					
						
							| 
									
										
										
										
											2023-04-11 17:45:51 +02:00
										 |  |  |     escapeRegExp, | 
					
						
							| 
									
										
										
										
											2023-05-29 00:19:54 +02:00
										 |  |  |     areObjectsEqual, | 
					
						
							| 
									
										
										
										
											2023-10-19 09:20:23 +02:00
										 |  |  |     copyHtmlToClipboard, | 
					
						
							| 
									
										
										
										
											2024-09-01 23:20:41 +03:00
										 |  |  |     createImageSrcUrl, | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     downloadSvg, | 
					
						
							| 
									
										
										
										
											2025-03-22 16:30:19 +02:00
										 |  |  |     downloadSvgAsPng, | 
					
						
							| 
									
										
										
										
											2024-11-09 22:16:00 +00:00
										 |  |  |     compareVersions, | 
					
						
							| 
									
										
										
										
											2025-01-04 21:52:41 +02:00
										 |  |  |     isUpdateAvailable, | 
					
						
							|  |  |  |     isLaunchBarConfig | 
					
						
							| 
									
										
										
										
											2020-05-20 00:03:33 +02:00
										 |  |  | }; |