diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index d731f03382..7019617714 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,10 +1,11 @@ import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; -import { SqlExecuteResponse } from "@triliumnext/commons"; +import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons"; import type { NativeImage, TouchBar } from "electron"; import { ColumnComponent } from "tabulator-tables"; import type { Attribute } from "../services/attribute_parser.js"; +import bundleService from "../services/bundle.js"; import froca from "../services/froca.js"; import { initLocale, t } from "../services/i18n.js"; import keyboardActionsService from "../services/keyboard_actions.js"; @@ -563,7 +564,7 @@ export class AppContext extends Component { */ async earlyInit() { await options.initializedPromise; - await initLocale(); + await initLocale((options.get("locale") || "en") as LOCALE_IDS); } setLayout(layout: Layout) { @@ -578,7 +579,6 @@ export class AppContext extends Component { this.tabManager.loadTabs(); - const bundleService = (await import("../services/bundle.js")).default; setTimeout(() => bundleService.executeStartupBundles(), 2000); } diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 12fd311bec..5fe7caf33c 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -1,5 +1,6 @@ import { getNoteIcon } from "@triliumnext/commons"; +import bundleService from "../services/bundle.js"; import cssClassManager from "../services/css_class_manager.js"; import type { Froca } from "../services/froca-interface.js"; import noteAttributeCache from "../services/note_attribute_cache.js"; @@ -1014,7 +1015,6 @@ export default class FNote { const env = this.getScriptEnv(); if (env === "frontend") { - const bundleService = (await import("../services/bundle.js")).default; return await bundleService.getAndExecuteBundle(this.noteId); } else if (env === "backend") { await server.post(`script/run/${this.noteId}`); diff --git a/apps/client/src/menus/electron_context_menu.ts b/apps/client/src/menus/electron_context_menu.ts index 6baba6a951..547a26d3d8 100644 --- a/apps/client/src/menus/electron_context_menu.ts +++ b/apps/client/src/menus/electron_context_menu.ts @@ -1,12 +1,14 @@ -import utils from "../services/utils.js"; -import options from "../services/options.js"; -import zoomService from "../components/zoom.js"; -import contextMenu, { type MenuItem } from "./context_menu.js"; -import { t } from "../services/i18n.js"; -import server from "../services/server.js"; -import * as clipboardExt from "../services/clipboard_ext.js"; import type { BrowserWindow } from "electron"; -import type { CommandNames, AppContext } from "../components/app_context.js"; + +import type { CommandNames } from "../components/app_context.js"; +import appContext from "../components/app_context.js"; +import zoomService from "../components/zoom.js"; +import * as clipboardExt from "../services/clipboard_ext.js"; +import { t } from "../services/i18n.js"; +import options from "../services/options.js"; +import server from "../services/server.js"; +import utils from "../services/utils.js"; +import contextMenu, { type MenuItem } from "./context_menu.js"; function setupContextMenu() { const electron = utils.dynamicRequire("electron"); @@ -15,8 +17,6 @@ function setupContextMenu() { // FIXME: Remove typecast once Electron is properly integrated. const { webContents } = remote.getCurrentWindow() as BrowserWindow; - let appContext: AppContext; - webContents.on("context-menu", (event, params) => { const { editFlags } = params; const hasText = params.selectionText.trim().length > 0; @@ -141,7 +141,7 @@ function setupContextMenu() { } // Replace the placeholder with the real search keyword. - let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); + const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); items.push({ kind: "separator" }); @@ -155,10 +155,6 @@ function setupContextMenu() { title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }), uiIcon: "bx bx-search", handler: async () => { - if (!appContext) { - appContext = (await import("../components/app_context.js")).default; - } - await appContext.triggerCommand("searchNotes", { searchString: params.selectionText }); diff --git a/apps/client/src/print.tsx b/apps/client/src/print.tsx index dc7817d9b5..c943413c49 100644 --- a/apps/client/src/print.tsx +++ b/apps/client/src/print.tsx @@ -4,6 +4,7 @@ import { useCallback, useLayoutEffect, useRef } from "preact/hooks"; import FNote from "./entities/fnote"; import content_renderer from "./services/content_renderer"; import { applyInlineMermaid } from "./services/content_renderer_text"; +import froca from "./services/froca"; import { dynamicRequire, isElectron } from "./services/utils"; import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList"; @@ -30,7 +31,6 @@ async function main() { if (!noteId) return; await import("./print.css"); - const froca = (await import("./services/froca")).default; const note = await froca.getNote(noteId); const bodyWrapper = document.createElement("div"); diff --git a/apps/client/src/services/bundle.ts b/apps/client/src/services/bundle.ts index 7cee01812b..fdee38ba34 100644 --- a/apps/client/src/services/bundle.ts +++ b/apps/client/src/services/bundle.ts @@ -26,7 +26,7 @@ type WithNoteId = T & { }; export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>; -async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) { +async function getAndExecuteBundle(noteId: string, originEntity: Entity | null = null, script: string | null = null, params: string | null = null) { const bundle = await server.post(`script/bundle/${noteId}`, { script, params diff --git a/apps/client/src/services/clipboard_ext.ts b/apps/client/src/services/clipboard_ext.ts index 9ab98af68f..16eea4ca49 100644 --- a/apps/client/src/services/clipboard_ext.ts +++ b/apps/client/src/services/clipboard_ext.ts @@ -1,3 +1,6 @@ +import { t } from "./i18n.js"; +import toast from "./toast.js"; + export function copyText(text: string) { if (!text) { return; @@ -6,29 +9,26 @@ export function copyText(text: string) { if (navigator.clipboard) { navigator.clipboard.writeText(text); return true; - } else { - // Fallback method: https://stackoverflow.com/a/72239825 - const textArea = document.createElement("textarea"); - textArea.value = text; - try { - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - return document.execCommand('copy'); - } finally { - document.body.removeChild(textArea); - } + } + // Fallback method: https://stackoverflow.com/a/72239825 + const textArea = document.createElement("textarea"); + textArea.value = text; + try { + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + return document.execCommand('copy'); + } finally { + document.body.removeChild(textArea); } + } catch (e) { console.warn(e); return false; } } -export async function copyTextWithToast(text: string) { - const t = (await import("./i18n.js")).t; - const toast = (await import("./toast.js")).default; - +export function copyTextWithToast(text: string) { if (copyText(text)) { toast.showMessage(t("clipboard.copy_success")); } else { diff --git a/apps/client/src/services/dialog.ts b/apps/client/src/services/dialog.ts index 8711ec1751..6888870fc9 100644 --- a/apps/client/src/services/dialog.ts +++ b/apps/client/src/services/dialog.ts @@ -1,9 +1,11 @@ import { Modal } from "bootstrap"; + import appContext from "../components/app_context.js"; import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js"; +import { InfoExtraProps } from "../widgets/dialogs/info.jsx"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import { focusSavedElement, saveFocusedElement } from "./focus.js"; -import { InfoExtraProps } from "../widgets/dialogs/info.jsx"; +import keyboardActionsService from "./keyboard_actions.js"; export async function openDialog($dialog: JQuery, closeActDialog = true, config?: Partial) { if (closeActDialog) { @@ -25,7 +27,6 @@ export async function openDialog($dialog: JQuery, closeActDialog = } }); - const keyboardActionsService = (await import("./keyboard_actions.js")).default; keyboardActionsService.updateDisplayedShortcuts($dialog); return $dialog; diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index ca6c792746..8be1eceada 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -1,14 +1,16 @@ -import LoadResults from "./load_results.js"; -import froca from "./froca.js"; -import utils from "./utils.js"; -import options from "./options.js"; -import noteAttributeCache from "./note_attribute_cache.js"; -import FBranch, { type FBranchRow } from "../entities/fbranch.js"; -import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; +import type { OptionNames } from "@triliumnext/commons"; + +import appContext from "../components/app_context.js"; import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; +import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; +import FBranch, { type FBranchRow } from "../entities/fbranch.js"; import type { default as FNote, FNoteRow } from "../entities/fnote.js"; import type { EntityChange } from "../server_types.js"; -import type { OptionNames } from "@triliumnext/commons"; +import froca from "./froca.js"; +import LoadResults from "./load_results.js"; +import noteAttributeCache from "./note_attribute_cache.js"; +import options from "./options.js"; +import utils from "./utils.js"; async function processEntityChanges(entityChanges: EntityChange[]) { const loadResults = new LoadResults(entityChanges); @@ -63,7 +65,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) { if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) { missingNoteIds.push((entity as FBranchRow).parentNoteId); } else if (entityName === "attributes") { - let attributeEntity = entity as FAttributeRow; + const attributeEntity = entity as FAttributeRow; if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) { missingNoteIds.push(attributeEntity.value); } @@ -79,7 +81,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) { noteAttributeCache.invalidate(); } - const appContext = (await import("../components/app_context.js")).default; await appContext.triggerEvent("entitiesReloaded", { loadResults }); } } diff --git a/apps/client/src/services/i18n.ts b/apps/client/src/services/i18n.ts index 5b5f38b762..0743596731 100644 --- a/apps/client/src/services/i18n.ts +++ b/apps/client/src/services/i18n.ts @@ -1,21 +1,14 @@ -import options from "./options.js"; +import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons"; import i18next from "i18next"; import i18nextHttpBackend from "i18next-http-backend"; -import server from "./server.js"; -import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons"; import { initReactI18next } from "react-i18next"; -let locales: Locale[] | null; - /** * A deferred promise that resolves when translations are initialized. */ -export let translationsInitializedPromise = $.Deferred(); +export const translationsInitializedPromise = $.Deferred(); -export async function initLocale() { - const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS; - - locales = await server.get("options/locales"); +export async function initLocale(locale: LOCALE_IDS = "en") { i18next.use(initReactI18next); await i18next.use(i18nextHttpBackend).init({ @@ -32,11 +25,7 @@ export async function initLocale() { } export function getAvailableLocales() { - if (!locales) { - throw new Error("Tried to load list of locales, but localization is not yet initialized.") - } - - return locales; + return LOCALES; } /** @@ -47,7 +36,7 @@ export function getAvailableLocales() { */ export function getLocaleById(localeId: string | null | undefined) { if (!localeId) return null; - return locales?.find((l) => l.id === localeId) ?? null; + return LOCALES.find((l) => l.id === localeId) ?? null; } export const t = i18next.t; diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index 010b57cae1..2d9d8273ae 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -1,3 +1,4 @@ +import { t } from "./i18n.js"; import utils, { isShare } from "./utils.js"; import ValidationError from "./validation_error.js"; @@ -32,8 +33,7 @@ async function getHeaders(headers?: Headers) { return {}; } - const appContext = (await import("../components/app_context.js")).default; - const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null; + const activeNoteContext = glob.appContext?.tabManager ? glob.appContext.tabManager.getActiveContext() : null; // headers need to be lowercase because node.js automatically converts them to lower case // also avoiding using underscores instead of dashes since nginx filters them out by default @@ -344,6 +344,7 @@ async function reportError(method: string, url: string, statusCode: number, resp } catch (e) {} } + // Dynamic import to avoid circular dependency (toast → app_context → options → server). const toastService = (await import("./toast.js")).default; const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-"; @@ -357,7 +358,6 @@ async function reportError(method: string, url: string, statusCode: number, resp ...response }); } else { - const { t } = await import("./i18n.js"); if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) { toastService.showPersistent({ id: "trafik-blocked", @@ -371,8 +371,7 @@ async function reportError(method: string, url: string, statusCode: number, resp t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }), 15_000); } - const { logError } = await import("./ws.js"); - logError(`${statusCode} ${method} ${url} - ${message}`); + window.logError(`${statusCode} ${method} ${url} - ${message}`); } } diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 30b5e4eaf7..67bbdd0e26 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -455,9 +455,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) { export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: { hoistedNoteId?: string; } = {}) { - // Dynamic import to avoid import issues in tests. - const appContext = (await import("../components/app_context.js")).default; - const activeContext = appContext.tabManager.getActiveContext(); + const activeContext = glob.appContext?.tabManager?.getActiveContext(); if (!activeContext) { return; } @@ -467,7 +465,7 @@ export async function openInReusableSplit(targetNoteId: string, targetViewMode: if (!existingSubcontext) { // The target split is not already open, open a new split with it. const { ntxId } = subContexts[subContexts.length - 1]; - appContext.triggerCommand("openNewNoteSplit", { + glob.appContext?.triggerCommand("openNewNoteSplit", { ntxId, notePath: targetNoteId, hoistedNoteId: openOpts.hoistedNoteId, diff --git a/apps/client/src/services/ws.ts b/apps/client/src/services/ws.ts index 1002abe9f7..47ed90341b 100644 --- a/apps/client/src/services/ws.ts +++ b/apps/client/src/services/ws.ts @@ -1,13 +1,15 @@ -import utils from "./utils.js"; -import toastService from "./toast.js"; -import server from "./server.js"; -import options from "./options.js"; -import frocaUpdater from "./froca_updater.js"; -import appContext from "../components/app_context.js"; -import { t } from "./i18n.js"; -import type { EntityChange } from "../server_types.js"; import { WebSocketMessage } from "@triliumnext/commons"; + +import appContext from "../components/app_context.js"; +import type { EntityChange } from "../server_types.js"; +import bundleService from "./bundle.js"; +import froca from "./froca.js"; +import frocaUpdater from "./froca_updater.js"; +import { t } from "./i18n.js"; +import options from "./options.js"; +import server from "./server.js"; import toast from "./toast.js"; +import utils from "./utils.js"; type MessageHandler = (message: WebSocketMessage) => void; let messageHandlers: MessageHandler[] = []; @@ -126,20 +128,14 @@ async function handleMessage(event: MessageEvent) { } else if (message.type === "frontend-update") { await executeFrontendUpdate(message.data.entityChanges); } else if (message.type === "sync-hash-check-failed") { - toastService.showError(t("ws.sync-check-failed"), 60000); + toast.showError(t("ws.sync-check-failed"), 60000); } else if (message.type === "consistency-checks-failed") { - toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000); + toast.showError(t("ws.consistency-checks-failed"), 50 * 60000); } else if (message.type === "api-log-messages") { appContext.triggerEvent("apiLogMessages", { noteId: message.noteId, messages: message.messages }); } else if (message.type === "toast") { - toastService.showMessage(message.message); + toast.showMessage(message.message); } else if (message.type === "execute-script") { - // TODO: Remove after porting the file - // @ts-ignore - const bundleService = (await import("./bundle.js")).default as any; - // TODO: Remove after porting the file - // @ts-ignore - const froca = (await import("./froca.js")).default as any; const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null; bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params); @@ -161,7 +157,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) { return new Promise((res, rej) => { entityChangeIdReachedListeners.push({ - desiredEntityChangeId: desiredEntityChangeId, + desiredEntityChangeId, resolvePromise: res, start: Date.now() }); @@ -205,7 +201,7 @@ async function consumeFrontendUpdateData() { } else { console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges); - toastService.showError(t("ws.encountered-error", { message: e.message })); + toast.showError(t("ws.encountered-error", { message: e.message })); } } diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts index ce5d7b9dc6..9e21dcb7b7 100644 --- a/apps/server/src/routes/api/options.ts +++ b/apps/server/src/routes/api/options.ts @@ -5,7 +5,7 @@ import type { Request } from "express"; import ValidationError from "../../errors/validation_error.js"; import config from "../../services/config.js"; -import { changeLanguage, getLocales } from "../../services/i18n.js"; +import { changeLanguage } from "../../services/i18n.js"; import log from "../../services/log.js"; import optionService from "../../services/options.js"; import searchService from "../../services/search/services/search.js"; @@ -192,10 +192,6 @@ function getUserThemes() { return ret; } -function getSupportedLocales() { - return getLocales(); -} - function isAllowed(name: string) { return (ALLOWED_OPTIONS as Set).has(name) || name.startsWith("keyboardShortcuts") @@ -207,6 +203,5 @@ export default { getOptions, updateOption, updateOptions, - getUserThemes, - getSupportedLocales + getUserThemes }; diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index 775a36e833..198fa5c22e 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -215,7 +215,6 @@ function register(app: express.Application) { apiRoute(PUT, "/api/options/:name/:value", optionsApiRoute.updateOption); apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions); apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes); - apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales); apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword); apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword); diff --git a/apps/server/src/services/i18n.ts b/apps/server/src/services/i18n.ts index f77ec93ef6..51cec3e2ed 100644 --- a/apps/server/src/services/i18n.ts +++ b/apps/server/src/services/i18n.ts @@ -4,7 +4,7 @@ import sql_init from "./sql_init.js"; import { join } from "path"; import { getResourceDir } from "./utils.js"; import hidden_subtree from "./hidden_subtree.js"; -import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type Locale, type LOCALE_IDS } from "@triliumnext/commons"; +import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type LOCALE_IDS } from "@triliumnext/commons"; export async function initializeTranslations() { const resourceDir = getResourceDir(); @@ -30,10 +30,6 @@ export function ordinal(date: Dayjs) { .format("Do"); } -export function getLocales(): Locale[] { - return LOCALES; -} - function getCurrentLanguage(): LOCALE_IDS { let language: string | null = null; if (sql_init.isDbInitialized()) {