726 lines
20 KiB
TypeScript
Raw Normal View History

2024-07-25 00:18:57 +03:00
import dayjs from "dayjs";
import { Modal } from "bootstrap";
2025-03-05 21:38:41 +02:00
import type { ViewScope } from "./link.js";
2024-07-25 00:18:57 +03:00
2024-07-25 20:47:33 +03:00
function reloadFrontendApp(reason?: string) {
2021-09-17 22:34:23 +02:00
if (reason) {
logInfo(`Frontend app reload: ${reason}`);
2021-09-17 22:34:23 +02:00
}
window.location.reload();
}
/**
* 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) {
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-24 23:37:55 -04: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("-");
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;
2025-03-02 20:47:57 +01:00
const startMon = i === startYear ? parseInt(start[1]) - 1 : 0;
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("-"));
}
}
return dates;
}
2024-07-25 00:18:57 +03:00
function padNum(num: number) {
return `${num <= 9 ? "0" : ""}${num}`;
}
2018-03-24 23:37:55 -04:00
2024-07-25 00:18:57 +03:00
function formatTime(date: Date) {
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`;
}
2018-03-24 23:37:55 -04:00
2024-07-25 00:18:57 +03:00
function formatTimeWithSeconds(date: Date) {
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`;
}
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" : ""}`;
2023-04-21 00:19:17 +02:00
const segments = [];
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) {
// 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);
}
2024-07-25 00:18:57 +03:00
/** this is producing local time! **/
function formatDateISO(date: Date) {
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
}
2024-07-25 00:18:57 +03:00
function formatDateTime(date: Date) {
return `${formatDate(date)} ${formatTime(date)}`;
}
function localNowDateTime() {
2025-01-09 18:07:02 +02:00
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() {
2019-02-09 19:25:55 +01:00
return !!(window && window.process && window.process.type);
}
function isMac() {
2025-01-09 18:07:02 +02:00
return navigator.platform.indexOf("Mac") > -1;
}
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) {
2025-01-09 18:07:02 +02:00
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}
2025-01-28 15:44:15 +02:00
function assertArguments<T>(...args: T[]) {
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
}
}
}
2024-07-25 00:18:57 +03:00
const entityMap: Record<string, string> = {
2025-01-09 18:07:02 +02:00
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
"/": "&#x2F;",
"`": "&#x60;",
"=": "&#x3D;"
};
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-24 23:37:55 -04:00
export function escapeQuotes(value: string) {
2025-03-02 20:47:57 +01:00
return value.replaceAll('"', "&quot;");
}
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`;
}
}
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
const obj: Record<string, R> = {};
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;
}
return obj;
}
2024-07-25 00:18:57 +03:00
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() {
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))
);
}
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))
);
}
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);
const expires = `; expires=${date.toUTCString()}`;
2018-12-29 00:09:16 +01:00
document.cookie = `${name}=${value || ""}${expires};`;
}
2024-07-25 00:18:57 +03:00
function getNoteTypeClass(type: string) {
return `type-${type}`;
}
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(";");
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;
}
}
2024-07-25 00:18:57 +03:00
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
2025-01-09 18:07:02 +02:00
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;
}
2024-07-25 00:18:57 +03:00
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
2022-01-10 17:09:20 +01:00
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
Modal.getOrCreateInstance($dialog[0]).show();
2025-01-09 18:07:02 +02:00
$dialog.on("hidden.bs.modal", () => {
2025-01-04 19:15:24 +02:00
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
2024-07-25 00:18:57 +03:00
// TODO: Fix once keyboard_actions is ported.
// @ts-ignore
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;
}
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") {
logError(`Got object of type '${typeof html}' where string was expected.`);
return false;
2020-03-26 16:59:40 +01:00
}
html = html.toLowerCase();
2025-01-09 18:07:02 +02:00
return (
!html.includes("<img") &&
!html.includes("<section") &&
// 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-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();
}
}
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);
}
}
2024-07-25 00:18:57 +03:00
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 {
// 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
return promise;
}
2020-07-29 23:34:49 +02:00
// better stack trace if created outside of promise
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
2020-07-29 23:34:49 +02: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>) {
// 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());
// 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/";
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
}
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) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
2025-03-18 18:44:48 +01:00
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext) {
return;
}
const subContexts = activeContext.getSubContexts();
2025-03-05 21:38:41 +02:00
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;
}
}
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
});
}
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, "");
}
const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
2024-07-25 00:18:57 +03:00
function isValidAttributeName(name: string) {
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-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");
}
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) {
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 }) {
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.
*
* @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
*/
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
2024-09-01 23:20:41 +03:00
const filename = `${nameWithoutExtension}.svg`;
2025-01-09 18:07:02 +02:00
const element = document.createElement("a");
element.setAttribute("href", `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
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);
}
/**
* 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
2025-01-09 18:07:02 +02:00
v1 = v1.replace(/^v/, "").split("-")[0];
v2 = v2.replace(/^v/, "").split("-")[0];
2025-01-09 18:07:02 +02:00
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;
}
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.
*/
function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
if (!latestVersion) {
return false;
}
return compareVersions(latestVersion, currentVersion) > 0;
}
function isLaunchBarConfig(noteId: string) {
2025-01-09 18:07:02 +02:00
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
}
export default {
2021-08-24 22:59:51 +02:00
reloadFrontendApp,
2025-02-01 11:04:49 +02:00
reloadTray,
parseDate,
getMonthsInDateRange,
formatDateISO,
formatDateTime,
2023-04-21 00:19:17 +02:00
formatTimeInterval,
2023-03-24 10:57:32 +01:00
formatSize,
localNowDateTime,
now,
isElectron,
isMac,
isCtrlKey,
assertArguments,
escapeHtml,
toObject,
randomString,
isMobile,
2018-12-29 00:09:16 +01:00
isDesktop,
setCookie,
getNoteTypeClass,
getMimeTypeClass,
closeActiveDialog,
openDialog,
saveFocusedElement,
focusSavedElement,
2019-11-08 23:09:57 +01:00
isHtmlEmpty,
clearBrowserCache,
copySelectionToClipboard,
2020-06-11 00:13:56 +02:00
dynamicRequire,
timeLimit,
initHelpDropdown,
initHelpButtons,
2021-12-20 17:30:47 +01:00
openHelp,
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,
copyHtmlToClipboard,
2024-09-01 23:20:41 +03:00
createImageSrcUrl,
downloadSvg,
compareVersions,
isUpdateAvailable,
isLaunchBarConfig
2020-05-20 00:03:33 +02:00
};