mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	chore(nx): move all monorepo-style in subfolder for processing
This commit is contained in:
		| @@ -1,846 +0,0 @@ | ||||
| import dayjs from "dayjs"; | ||||
| import { Modal } from "bootstrap"; | ||||
| import type { ViewScope } from "./link.js"; | ||||
|  | ||||
| const SVG_MIME = "image/svg+xml"; | ||||
|  | ||||
| function reloadFrontendApp(reason?: string) { | ||||
|     if (reason) { | ||||
|         logInfo(`Frontend app reload: ${reason}`); | ||||
|     } | ||||
|  | ||||
|     window.location.reload(); | ||||
| } | ||||
|  | ||||
| function restartDesktopApp() { | ||||
|     if (!isElectron()) { | ||||
|         reloadFrontendApp(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const app = dynamicRequire("@electron/remote").app; | ||||
|     app.relaunch(); | ||||
|     app.exit(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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. | ||||
|  */ | ||||
| function reloadTray() { | ||||
|     if (!isElectron()) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const { ipcRenderer } = dynamicRequire("electron"); | ||||
|     ipcRenderer.send("reload-tray"); | ||||
| } | ||||
|  | ||||
| function parseDate(str: string) { | ||||
|     try { | ||||
|         return new Date(Date.parse(str)); | ||||
|     } catch (e: any) { | ||||
|         throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Source: https://stackoverflow.com/a/30465299/4898894 | ||||
| function getMonthsInDateRange(startDate: string, endDate: string) { | ||||
|     const start = startDate.split("-"); | ||||
|     const end = endDate.split("-"); | ||||
|     const startYear = parseInt(start[0]); | ||||
|     const endYear = parseInt(end[0]); | ||||
|     const dates = []; | ||||
|  | ||||
|     for (let i = startYear; i <= endYear; i++) { | ||||
|         const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1; | ||||
|         const startMon = i === startYear ? parseInt(start[1]) - 1 : 0; | ||||
|  | ||||
|         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("-")); | ||||
|         } | ||||
|     } | ||||
|     return dates; | ||||
| } | ||||
|  | ||||
| function padNum(num: number) { | ||||
|     return `${num <= 9 ? "0" : ""}${num}`; | ||||
| } | ||||
|  | ||||
| function formatTime(date: Date) { | ||||
|     return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`; | ||||
| } | ||||
|  | ||||
| function formatTimeWithSeconds(date: Date) { | ||||
|     return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`; | ||||
| } | ||||
|  | ||||
| function formatTimeInterval(ms: number) { | ||||
|     const seconds = Math.round(ms / 1000); | ||||
|     const minutes = Math.floor(seconds / 60); | ||||
|     const hours = Math.floor(minutes / 60); | ||||
|     const days = Math.floor(hours / 24); | ||||
|     const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? "s" : ""}`; | ||||
|     const segments = []; | ||||
|  | ||||
|     if (days > 0) { | ||||
|         segments.push(plural(days, "day")); | ||||
|     } | ||||
|  | ||||
|     if (days < 2) { | ||||
|         if (hours % 24 > 0) { | ||||
|             segments.push(plural(hours % 24, "hour")); | ||||
|         } | ||||
|  | ||||
|         if (hours < 4) { | ||||
|             if (minutes % 60 > 0) { | ||||
|                 segments.push(plural(minutes % 60, "minute")); | ||||
|             } | ||||
|  | ||||
|             if (minutes < 5) { | ||||
|                 if (seconds % 60 > 0) { | ||||
|                     segments.push(plural(seconds % 60, "second")); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return segments.join(", "); | ||||
| } | ||||
|  | ||||
| /** this is producing local time! **/ | ||||
| function formatDate(date: Date) { | ||||
|     //    return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear(); | ||||
|     // instead of european format we'll just use ISO as that's pretty unambiguous | ||||
|  | ||||
|     return formatDateISO(date); | ||||
| } | ||||
|  | ||||
| /** this is producing local time! **/ | ||||
| function formatDateISO(date: Date) { | ||||
|     return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`; | ||||
| } | ||||
|  | ||||
| function formatDateTime(date: Date) { | ||||
|     return `${formatDate(date)} ${formatTime(date)}`; | ||||
| } | ||||
|  | ||||
| function localNowDateTime() { | ||||
|     return dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZZ"); | ||||
| } | ||||
|  | ||||
| function now() { | ||||
|     return formatTimeWithSeconds(new Date()); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns `true` if the client is currently running under Electron, or `false` if running in a web browser. | ||||
|  */ | ||||
| function isElectron() { | ||||
|     return !!(window && window.process && window.process.type); | ||||
| } | ||||
|  | ||||
| function isMac() { | ||||
|     return navigator.platform.indexOf("Mac") > -1; | ||||
| } | ||||
|  | ||||
| export const hasTouchBar = (isMac() && isElectron()); | ||||
|  | ||||
| function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) { | ||||
|     return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); | ||||
| } | ||||
|  | ||||
| function assertArguments<T>(...args: T[]) { | ||||
|     for (const i in args) { | ||||
|         if (!args[i]) { | ||||
|             console.trace(`Argument idx#${i} should not be falsy: ${args[i]}`); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| const entityMap: Record<string, string> = { | ||||
|     "&": "&", | ||||
|     "<": "<", | ||||
|     ">": ">", | ||||
|     '"': """, | ||||
|     "'": "'", | ||||
|     "/": "/", | ||||
|     "`": "`", | ||||
|     "=": "=" | ||||
| }; | ||||
|  | ||||
| function escapeHtml(str: string) { | ||||
|     return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]); | ||||
| } | ||||
|  | ||||
| export function escapeQuotes(value: string) { | ||||
|     return value.replaceAll('"', """); | ||||
| } | ||||
|  | ||||
| function formatSize(size: number) { | ||||
|     size = Math.max(Math.round(size / 1024), 1); | ||||
|  | ||||
|     if (size < 1024) { | ||||
|         return `${size} KiB`; | ||||
|     } else { | ||||
|         return `${Math.round(size / 102.4) / 10} MiB`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) { | ||||
|     const obj: Record<string, R> = {}; | ||||
|  | ||||
|     for (const item of array) { | ||||
|         const [key, value] = fn(item); | ||||
|  | ||||
|         obj[key] = value; | ||||
|     } | ||||
|  | ||||
|     return obj; | ||||
| } | ||||
|  | ||||
| function randomString(len: number) { | ||||
|     let text = ""; | ||||
|     const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||||
|  | ||||
|     for (let i = 0; i < len; i++) { | ||||
|         text += possible.charAt(Math.floor(Math.random() * possible.length)); | ||||
|     } | ||||
|  | ||||
|     return text; | ||||
| } | ||||
|  | ||||
| function isMobile() { | ||||
|     return ( | ||||
|         window.glob?.device === "mobile" || | ||||
|         // window.glob.device is not available in setup | ||||
|         (!window.glob?.device && /Mobi/.test(navigator.userAgent)) | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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); | ||||
| } | ||||
|  | ||||
| function isDesktop() { | ||||
|     return ( | ||||
|         window.glob?.device === "desktop" || | ||||
|         // window.glob.device is not available in setup | ||||
|         (!window.glob?.device && !/Mobi/.test(navigator.userAgent)) | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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) { | ||||
|     const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000); | ||||
|     const expires = `; expires=${date.toUTCString()}`; | ||||
|  | ||||
|     document.cookie = `${name}=${value || ""}${expires};`; | ||||
| } | ||||
|  | ||||
| function getNoteTypeClass(type: string) { | ||||
|     return `type-${type}`; | ||||
| } | ||||
|  | ||||
| function getMimeTypeClass(mime: string) { | ||||
|     if (!mime) { | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     const semicolonIdx = mime.indexOf(";"); | ||||
|  | ||||
|     if (semicolonIdx !== -1) { | ||||
|         // stripping everything following the semicolon | ||||
|         mime = mime.substr(0, semicolonIdx); | ||||
|     } | ||||
|  | ||||
|     return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; | ||||
| } | ||||
|  | ||||
| function closeActiveDialog() { | ||||
|     if (glob.activeDialog) { | ||||
|         Modal.getOrCreateInstance(glob.activeDialog[0]).hide(); | ||||
|         glob.activeDialog = null; | ||||
|     } | ||||
| } | ||||
|  | ||||
| let $lastFocusedElement: JQuery<HTMLElement> | null; | ||||
|  | ||||
| // perhaps there should be saved focused element per tab? | ||||
| function saveFocusedElement() { | ||||
|     $lastFocusedElement = $(":focus"); | ||||
| } | ||||
|  | ||||
| function focusSavedElement() { | ||||
|     if (!$lastFocusedElement) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if ($lastFocusedElement.hasClass("ck")) { | ||||
|         // must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607 | ||||
|         // the bug manifests itself in resetting the cursor position to the first character - jumping above | ||||
|  | ||||
|         const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance"); | ||||
|  | ||||
|         if (editor) { | ||||
|             editor.editing.view.focus(); | ||||
|         } else { | ||||
|             console.log("Could not find CKEditor instance to focus last element"); | ||||
|         } | ||||
|     } else { | ||||
|         $lastFocusedElement.focus(); | ||||
|     } | ||||
|  | ||||
|     $lastFocusedElement = null; | ||||
| } | ||||
|  | ||||
| async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) { | ||||
|     if (closeActDialog) { | ||||
|         closeActiveDialog(); | ||||
|         glob.activeDialog = $dialog; | ||||
|     } | ||||
|  | ||||
|     saveFocusedElement(); | ||||
|     Modal.getOrCreateInstance($dialog[0]).show(); | ||||
|  | ||||
|     $dialog.on("hidden.bs.modal", () => { | ||||
|         const $autocompleteEl = $(".aa-input"); | ||||
|         if ("autocomplete" in $autocompleteEl) { | ||||
|             $autocompleteEl.autocomplete("close"); | ||||
|         } | ||||
|  | ||||
|         if (!glob.activeDialog || glob.activeDialog === $dialog) { | ||||
|             focusSavedElement(); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // TODO: Fix once keyboard_actions is ported. | ||||
|     // @ts-ignore | ||||
|     const keyboardActionsService = (await import("./keyboard_actions.js")).default; | ||||
|     keyboardActionsService.updateDisplayedShortcuts($dialog); | ||||
|  | ||||
|     return $dialog; | ||||
| } | ||||
|  | ||||
| function isHtmlEmpty(html: string) { | ||||
|     if (!html) { | ||||
|         return true; | ||||
|     } else if (typeof html !== "string") { | ||||
|         logError(`Got object of type '${typeof html}' where string was expected.`); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     html = html.toLowerCase(); | ||||
|  | ||||
|     return ( | ||||
|         !html.includes("<img") && | ||||
|         !html.includes("<section") && | ||||
|         // the line below will actually attempt to load images so better to check for images first | ||||
|         $("<div>").html(html).text().trim().length === 0 | ||||
|     ); | ||||
| } | ||||
|  | ||||
| async function clearBrowserCache() { | ||||
|     if (isElectron()) { | ||||
|         const win = dynamicRequire("@electron/remote").getCurrentWindow(); | ||||
|         await win.webContents.session.clearCache(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function copySelectionToClipboard() { | ||||
|     const text = window?.getSelection()?.toString(); | ||||
|     if (text && navigator.clipboard) { | ||||
|         navigator.clipboard.writeText(text); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function dynamicRequire(moduleName: string) { | ||||
|     if (typeof __non_webpack_require__ !== "undefined") { | ||||
|         return __non_webpack_require__(moduleName); | ||||
|     } else { | ||||
|         // 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}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string) { | ||||
|     if (!promise || !promise.then) { | ||||
|         // it's not actually a promise | ||||
|         return promise; | ||||
|     } | ||||
|  | ||||
|     // better stack trace if created outside of promise | ||||
|     const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`); | ||||
|  | ||||
|     return new Promise<T>((res, rej) => { | ||||
|         let resolved = false; | ||||
|  | ||||
|         promise.then((result) => { | ||||
|             resolved = true; | ||||
|  | ||||
|             res(result); | ||||
|         }); | ||||
|  | ||||
|         setTimeout(() => { | ||||
|             if (!resolved) { | ||||
|                 rej(error); | ||||
|             } | ||||
|         }, limitMs); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function initHelpDropdown($el: JQuery<HTMLElement>) { | ||||
|     // stop inside clicks from closing the menu | ||||
|     const $dropdownMenu = $el.find(".help-dropdown .dropdown-menu"); | ||||
|     $dropdownMenu.on("click", (e) => e.stopPropagation()); | ||||
|  | ||||
|     // previous propagation stop will also block help buttons from being opened, so we need to re-init for this element | ||||
|     initHelpButtons($dropdownMenu); | ||||
| } | ||||
|  | ||||
| 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) { | ||||
|         const url = wikiBaseUrl + helpPage; | ||||
|  | ||||
|         window.open(url, "_blank"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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 activeContext = appContext.tabManager.getActiveContext(); | ||||
|         if (!activeContext) { | ||||
|             return; | ||||
|         } | ||||
|         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 }); | ||||
|         } | ||||
|         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) => { | ||||
|         openHelp($(e.target).closest("[data-help-page]")); | ||||
|         openInAppHelp($(e.target).closest("[data-in-app-help]")); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function filterAttributeName(name: string) { | ||||
|     return name.replace(/[^\p{L}\p{N}_:]/gu, ""); | ||||
| } | ||||
|  | ||||
| const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); | ||||
|  | ||||
| function isValidAttributeName(name: string) { | ||||
|     return ATTR_NAME_MATCHER.test(name); | ||||
| } | ||||
|  | ||||
| function sleep(time_ms: number) { | ||||
|     return new Promise((resolve) => { | ||||
|         setTimeout(resolve, time_ms); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function escapeRegExp(str: string) { | ||||
|     return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); | ||||
| } | ||||
|  | ||||
| function areObjectsEqual(...args: unknown[]) { | ||||
|     let i; | ||||
|     let l; | ||||
|     let leftChain: Object[]; | ||||
|     let rightChain: Object[]; | ||||
|  | ||||
|     function compare2Objects(x: unknown, y: unknown) { | ||||
|         let p; | ||||
|  | ||||
|         // remember that NaN === NaN returns false | ||||
|         // and isNaN(undefined) returns true | ||||
|         if (typeof x === "number" && typeof y === "number" && isNaN(x) && isNaN(y)) { | ||||
|             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 | ||||
|         if ( | ||||
|             (typeof x === "function" && typeof y === "function") || | ||||
|             (x instanceof Date && y instanceof Date) || | ||||
|             (x instanceof RegExp && y instanceof RegExp) || | ||||
|             (x instanceof String && y instanceof String) || | ||||
|             (x instanceof Number && y instanceof Number) | ||||
|         ) { | ||||
|             return x.toString() === y.toString(); | ||||
|         } | ||||
|  | ||||
|         // At last, checking prototypes as good as we can | ||||
|         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; | ||||
|         } | ||||
|  | ||||
|         if ((x as any).prototype !== (y as any).prototype) { | ||||
|             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; | ||||
|             } else if (typeof (y as any)[p] !== typeof (x as any)[p]) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for (p in x) { | ||||
|             if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { | ||||
|                 return false; | ||||
|             } else if (typeof (y as any)[p] !== typeof (x as any)[p]) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             switch (typeof (x as any)[p]) { | ||||
|                 case "object": | ||||
|                 case "function": | ||||
|                     leftChain.push(x); | ||||
|                     rightChain.push(y); | ||||
|  | ||||
|                     if (!compare2Objects((x as any)[p], (y as any)[p])) { | ||||
|                         return false; | ||||
|                     } | ||||
|  | ||||
|                     leftChain.pop(); | ||||
|                     rightChain.pop(); | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     if ((x as any)[p] !== (y as any)[p]) { | ||||
|                         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; | ||||
| } | ||||
|  | ||||
| function copyHtmlToClipboard(content: string) { | ||||
|     function listener(e: ClipboardEvent) { | ||||
|         if (e.clipboardData) { | ||||
|             e.clipboardData.setData("text/html", content); | ||||
|             e.clipboardData.setData("text/plain", content); | ||||
|         } | ||||
|         e.preventDefault(); | ||||
|     } | ||||
|     document.addEventListener("copy", listener); | ||||
|     document.execCommand("copy"); | ||||
|     document.removeEventListener("copy", listener); | ||||
| } | ||||
|  | ||||
| // TODO: Set to FNote once the file is ported. | ||||
| function createImageSrcUrl(note: { noteId: string; title: string }) { | ||||
|     return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Given a string representation of an SVG, triggers a download of the file on the client device. | ||||
|  * | ||||
|  * @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it. | ||||
|  * @param svgContent the content of the SVG file download. | ||||
|  */ | ||||
| function downloadSvg(nameWithoutExtension: string, svgContent: string) { | ||||
|     const filename = `${nameWithoutExtension}.svg`; | ||||
|     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) { | ||||
|     const element = document.createElement("a"); | ||||
|     element.setAttribute("href", dataUrl); | ||||
|     element.setAttribute("download", fileName); | ||||
|  | ||||
|     element.style.display = "none"; | ||||
|     document.body.appendChild(element); | ||||
|  | ||||
|     element.click(); | ||||
|  | ||||
|     document.body.removeChild(element); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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. | ||||
|  * @returns a promise which resolves if the operation was successful, or rejects if it failed (permissions issue or some other issue). | ||||
|  */ | ||||
| function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) { | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|         // First, we need to determine the width and the height from the input SVG. | ||||
|         const result = getSizeFromSvg(svgContent); | ||||
|         if (!result) { | ||||
|             reject(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Convert the image to a blob. | ||||
|         const { width, height } = result; | ||||
|  | ||||
|         // Create an image element and load the SVG. | ||||
|         const imageEl = new Image(); | ||||
|         imageEl.width = width; | ||||
|         imageEl.height = height; | ||||
|         imageEl.crossOrigin = "anonymous"; | ||||
|         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(); | ||||
|                 } | ||||
|  | ||||
|                 ctx?.drawImage(imageEl, 0, 0); | ||||
|  | ||||
|                 const imgUri = canvasEl.toDataURL("image/png") | ||||
|                 triggerDownload(`${nameWithoutExtension}.png`, imgUri); | ||||
|                 document.body.removeChild(canvasEl); | ||||
|                 resolve(); | ||||
|             } catch (e) { | ||||
|                 console.warn(e); | ||||
|                 reject(); | ||||
|             } | ||||
|         }; | ||||
|         imageEl.onerror = (e) => reject(e); | ||||
|         imageEl.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| 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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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 | ||||
|  * | ||||
|  * @param v1 First version string | ||||
|  * @param v2 Second version string | ||||
|  * @returns | ||||
|  */ | ||||
| function compareVersions(v1: string, v2: string): number { | ||||
|     // Remove 'v' prefix and everything after dash if present | ||||
|     v1 = v1.replace(/^v/, "").split("-")[0]; | ||||
|     v2 = v2.replace(/^v/, "").split("-")[0]; | ||||
|  | ||||
|     const v1parts = v1.split(".").map(Number); | ||||
|     const v2parts = v2.split(".").map(Number); | ||||
|  | ||||
|     // Pad shorter version with zeros | ||||
|     while (v1parts.length < 3) v1parts.push(0); | ||||
|     while (v2parts.length < 3) v2parts.push(0); | ||||
|  | ||||
|     // Compare major version | ||||
|     if (v1parts[0] !== v2parts[0]) { | ||||
|         return v1parts[0] > v2parts[0] ? 1 : -1; | ||||
|     } | ||||
|  | ||||
|     // Compare minor version | ||||
|     if (v1parts[1] !== v2parts[1]) { | ||||
|         return v1parts[1] > v2parts[1] ? 1 : -1; | ||||
|     } | ||||
|  | ||||
|     // Compare patch version | ||||
|     if (v1parts[2] !== v2parts[2]) { | ||||
|         return v1parts[2] > v2parts[2] ? 1 : -1; | ||||
|     } | ||||
|  | ||||
|     return 0; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Compares two semantic version strings and returns `true` if the latest version is greater than the current version. | ||||
|  */ | ||||
| function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean { | ||||
|     if (!latestVersion) { | ||||
|         return false; | ||||
|     } | ||||
|     return compareVersions(latestVersion, currentVersion) > 0; | ||||
| } | ||||
|  | ||||
| function isLaunchBarConfig(noteId: string) { | ||||
|     return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     reloadFrontendApp, | ||||
|     restartDesktopApp, | ||||
|     reloadTray, | ||||
|     parseDate, | ||||
|     getMonthsInDateRange, | ||||
|     formatDateISO, | ||||
|     formatDateTime, | ||||
|     formatTimeInterval, | ||||
|     formatSize, | ||||
|     localNowDateTime, | ||||
|     now, | ||||
|     isElectron, | ||||
|     isMac, | ||||
|     isCtrlKey, | ||||
|     assertArguments, | ||||
|     escapeHtml, | ||||
|     toObject, | ||||
|     randomString, | ||||
|     isMobile, | ||||
|     isDesktop, | ||||
|     setCookie, | ||||
|     getNoteTypeClass, | ||||
|     getMimeTypeClass, | ||||
|     closeActiveDialog, | ||||
|     openDialog, | ||||
|     saveFocusedElement, | ||||
|     focusSavedElement, | ||||
|     isHtmlEmpty, | ||||
|     clearBrowserCache, | ||||
|     copySelectionToClipboard, | ||||
|     dynamicRequire, | ||||
|     timeLimit, | ||||
|     initHelpDropdown, | ||||
|     initHelpButtons, | ||||
|     openHelp, | ||||
|     filterAttributeName, | ||||
|     isValidAttributeName, | ||||
|     sleep, | ||||
|     escapeRegExp, | ||||
|     areObjectsEqual, | ||||
|     copyHtmlToClipboard, | ||||
|     createImageSrcUrl, | ||||
|     downloadSvg, | ||||
|     downloadSvgAsPng, | ||||
|     compareVersions, | ||||
|     isUpdateAvailable, | ||||
|     isLaunchBarConfig | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user