Merge remote-tracking branch 'origin/develop' into feat/add-rootless-dockerfiles

This commit is contained in:
Elian Doran
2025-05-25 21:22:31 +03:00
72 changed files with 1325 additions and 1999 deletions

View File

@@ -137,7 +137,7 @@ export default async function buildApp() {
startScheduledCleanup();
if (utils.isElectron) {
(await import("@electron/remote/main")).initialize();
(await import("@electron/remote/main/index.js")).initialize();
}
return app;

View File

@@ -21,7 +21,7 @@
<body>
<div class="container login-page">
<div class="col-xs-12 col-sm-10 col-md-6 col-lg-4 col-xl-4 mx-auto pt-4">
<img class="img-fluid d-block mx-auto" style="height: 8rem;" src="<%= assetPath %>/images/icon-color.svg" aria-hidden="true" draggable="false" >
<img class="img-fluid d-block mx-auto" style="height: 8rem;" src="<%= assetPathFragment %>/images/icon-color.svg" aria-hidden="true" draggable="false" >
<h1 class="text-center"><%= t("login.heading") %></h1>
<% if (ssoEnabled) { %>

View File

@@ -36,7 +36,7 @@ interface DateLimits {
maxDate: string;
}
interface SimilarNote {
export interface SimilarNote {
score: number;
notePath: string[];
noteId: string;

View File

@@ -6,7 +6,7 @@ import etapiTokenService from "../services/etapi_tokens.js";
import config from "../services/config.js";
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
import type { ValidatorMap } from "./etapi-interface.js";
import type { ApiRequestHandler } from "../routes/routes.js";
import type { ApiRequestHandler } from "../routes/route_api.js";
const GENERIC_CODE = "GENERIC";
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";

View File

@@ -116,7 +116,7 @@ function getLinkMap(req: Request) {
}).notes;
}
const noteIds = new Set(unfilteredNotes.filter((note) => ignoreExcludeFromNoteMap || !note.isLabelTruthy("excludeFromNoteMap")).map((note) => note.noteId));
const noteIds = new Set<string>(unfilteredNotes.filter((note) => ignoreExcludeFromNoteMap || !note.isLabelTruthy("excludeFromNoteMap")).map((note) => note.noteId));
if (mapRootNote.type === "search") {
noteIds.delete(mapRootNote.noteId);

View File

@@ -118,7 +118,7 @@ function createNote(req: Request) {
throw new ValidationError("Missing or incorrect type for target branch ID.");
}
const { note, branch } = noteService.createNewNoteWithTarget(target, targetBranchId, params);
const { note, branch } = noteService.createNewNoteWithTarget(target, String(targetBranchId), params);
return {
note,

View File

@@ -1,10 +1,10 @@
import { assetUrlFragment } from "../services/asset_path.js";
import path from "path";
import { fileURLToPath } from "url";
import express from "express";
import { getResourceDir, isDev } from "../services/utils.js";
import type serveStatic from "serve-static";
import proxy from "express-http-proxy";
import { existsSync } from "fs";
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
if (!isDev) {
@@ -17,7 +17,7 @@ const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOp
};
async function register(app: express.Application) {
const srcRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const srcRoot = path.join(__dirname, "..");
const resourceDir = getResourceDir();
if (isDev) {
@@ -29,14 +29,19 @@ async function register(app: express.Application) {
proxyReqPathResolver: (req) => "/" + assetUrlFragment + `/@fs` + req.url
}));
} else {
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(resourceDir, "public", "src")));
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(resourceDir, "public", "stylesheets")));
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(resourceDir, "public", "libraries")));
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(resourceDir, "public", "fonts")));
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(resourceDir, "public", "translations")));
app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
app.use(`/node_modules/`, persistentCacheStatic(path.join(resourceDir, "public/node_modules")));
const publicDir = path.join(resourceDir, "public");
if (!existsSync(publicDir)) {
throw new Error("Public directory is missing at: " + path.resolve(publicDir));
}
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(publicDir, "src")));
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(publicDir, "stylesheets")));
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(publicDir, "libraries")));
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(publicDir, "fonts")));
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations")));
app.use(`/node_modules/`, persistentCacheStatic(path.join(publicDir, "node_modules")));
}
app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
app.use(`/${assetUrlFragment}/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes")));
app.use(`/assets/vX/fonts`, express.static(path.join(srcRoot, "public/fonts")));
app.use(`/assets/vX/images`, express.static(path.join(srcRoot, "..", "images")));

View File

@@ -1,4 +1,4 @@
import { ipcMain } from "electron";
import electron from "electron";
interface Response {
statusCode: number;
@@ -10,7 +10,7 @@ interface Response {
}
function init(app: Express.Application) {
ipcMain.on("server-request", (event, arg) => {
electron.ipcMain.on("server-request", (event, arg) => {
const req = {
url: arg.url,
method: arg.method,
@@ -50,7 +50,7 @@ function init(app: Express.Application) {
}
};
return app._router.handle(req, res, () => {});
return (app as any)._router.handle(req, res, () => {});
});
}

View File

@@ -3,7 +3,7 @@ import optionService from "../services/options.js";
import myScryptService from "../services/encryption/my_scrypt.js";
import log from "../services/log.js";
import passwordService from "../services/encryption/password.js";
import assetPath from "../services/asset_path.js";
import assetPath, { assetUrlFragment } from "../services/asset_path.js";
import appPath from "../services/app_path.js";
import ValidationError from "../errors/validation_error.js";
import type { Request, Response } from 'express';
@@ -13,12 +13,14 @@ import openID from '../services/open_id.js';
import openIDEncryption from '../services/encryption/open_id_encryption.js';
function loginPage(req: Request, res: Response) {
// Login page is triggered twice. Once here, and another time if the password is failed.
res.render('login', {
wrongPassword: false,
wrongTotp: false,
totpEnabled: totp.isTotpEnabled(),
ssoEnabled: openID.isOpenIDEnabled(),
assetPath: assetPath,
assetPathFragment: assetUrlFragment,
appPath: appPath,
});
}
@@ -169,6 +171,7 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
totpEnabled: totp.isTotpEnabled(),
ssoEnabled: openID.isOpenIDEnabled(),
assetPath: assetPath,
assetPathFragment: assetUrlFragment,
appPath: appPath,
});
}

View File

@@ -1,4 +1,4 @@
import express from "express";
import express, { type RequestHandler } from "express";
import multer from "multer";
import log from "../services/log.js";
import cls from "../services/cls.js";
@@ -166,7 +166,7 @@ function handleException(e: unknown | Error, method: HttpMethod, path: string, r
}
export function createUploadMiddleware() {
export function createUploadMiddleware(): RequestHandler {
const multerOptions: multer.Options = {
fileFilter: (req: express.Request, file, cb) => {
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.

View File

@@ -3,6 +3,7 @@ import session, { Store } from "express-session";
import sessionSecret from "../services/session_secret.js";
import config from "../services/config.js";
import log from "../services/log.js";
import type express from "express";
class SQLiteSessionStore extends Store {
@@ -51,7 +52,7 @@ class SQLiteSessionStore extends Store {
}
const sessionParser = session({
const sessionParser: express.RequestHandler = session({
secret: sessionSecret,
resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request.
saveUninitialized: false, // true forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified.

View File

@@ -1,5 +1,5 @@
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
import fs from "fs-extra";
import fs from "fs";
import dataDir from "./data_dir.js";
import dateUtils from "./date_utils.js";
import Database from "better-sqlite3";

View File

@@ -224,14 +224,14 @@ export interface Api {
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getDayNote(date: string, rootNote?: BNote): Promise<BNote | null>;
getDayNote(date: string, rootNote?: BNote): BNote | null;
/**
* Returns today's day note. If such note doesn't exist, it is created.
*
* @param rootNote specify calendar root note, normally leave empty to use the default calendar
*/
getTodayNote(rootNote?: BNote): Promise<BNote | null>;
getTodayNote(rootNote?: BNote): BNote | null;
/**
* Returns note for the first date of the week of the given date.
@@ -239,7 +239,7 @@ export interface Api {
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getWeekFirstDayNote(date: string, rootNote: BNote): Promise<BNote | null>;
getWeekFirstDayNote(date: string, rootNote: BNote): BNote | null;
/**
* Returns week note for given date. If such a note doesn't exist, it is created.
@@ -247,7 +247,7 @@ export interface Api {
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getWeekNote(date: string, rootNote: BNote): Promise<BNote | null>;
getWeekNote(date: string, rootNote: BNote): BNote | null;
/**
* Returns month note for given date. If such a note doesn't exist, it is created.
@@ -255,7 +255,7 @@ export interface Api {
* @param date in YYYY-MM format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getMonthNote(date: string, rootNote: BNote): Promise<BNote | null>;
getMonthNote(date: string, rootNote: BNote): BNote | null;
/**
* Returns quarter note for given date. If such a note doesn't exist, it is created.
@@ -263,7 +263,7 @@ export interface Api {
* @param date in YYYY-MM format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getQuarterNote(date: string, rootNote: BNote): Promise<BNote | null>;
getQuarterNote(date: string, rootNote: BNote): BNote | null;
/**
* Returns year note for given year. If such a note doesn't exist, it is created.

View File

@@ -2,7 +2,7 @@
import dateUtils from "./date_utils.js";
import optionService from "./options.js";
import fs from "fs-extra";
import fs from "fs";
import dataDir from "./data_dir.js";
import log from "./log.js";
import syncMutexService from "./sync_mutex.js";

View File

@@ -4,19 +4,26 @@ import sql_init from "./sql_init.js";
import { join } from "path";
import { getResourceDir } from "./utils.js";
import hidden_subtree from "./hidden_subtree.js";
import { LOCALES, type Locale } from "@triliumnext/commons";
import { LOCALES, type Locale, type LOCALE_IDS } from "@triliumnext/commons";
import dayjs, { Dayjs } from "dayjs";
const DAYJS_LOCALE_MAP: Record<string, string> = {
cn: "zh-cn",
tw: "zh-tw"
};
let dayjsLocale: string;
const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs/locale/en.js")>> = {
"ar": () => import("dayjs/locale/ar.js"),
"cn": () => import("dayjs/locale/zh-cn.js"),
"de": () => import("dayjs/locale/de.js"),
"en": () => import("dayjs/locale/en.js"),
"es": () => import("dayjs/locale/es.js"),
"fa": () => import("dayjs/locale/fa.js"),
"fr": () => import("dayjs/locale/fr.js"),
"he": () => import("dayjs/locale/he.js"),
"ku": () => import("dayjs/locale/ku.js"),
"ro": () => import("dayjs/locale/ro.js"),
"tw": () => import("dayjs/locale/zh-tw.js")
}
export async function initializeTranslations() {
const resourceDir = getResourceDir();
const Backend = (await import("i18next-fs-backend")).default;
const Backend = (await import("i18next-fs-backend/cjs")).default;
const locale = getCurrentLanguage();
// Initialize translations
@@ -30,12 +37,7 @@ export async function initializeTranslations() {
});
// Initialize dayjs locale.
dayjsLocale = DAYJS_LOCALE_MAP[locale] ?? locale;
try {
await import(`dayjs/locale/${dayjsLocale}.js`);
} catch (err) {
console.warn(`Could not load locale ${dayjsLocale}`, err);
}
const dayjsLocale = await DAYJS_LOADER[locale]();
dayjs.locale(dayjsLocale);
}
@@ -48,8 +50,8 @@ export function getLocales(): Locale[] {
return LOCALES;
}
function getCurrentLanguage() {
let language;
function getCurrentLanguage(): LOCALE_IDS {
let language: string;
if (sql_init.isDbInitialized()) {
language = options.getOptionOrNull("locale");
}
@@ -59,7 +61,7 @@ function getCurrentLanguage() {
language = "en";
}
return language;
return language as LOCALE_IDS;
}
export async function changeLanguage(locale: string) {

View File

@@ -194,9 +194,43 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
it("converts multi-line display math expressions into Mathtex format", () => {
const input = `$$
\\sqrt{x^{2}+1} \\
+ \\frac{1}{2}
$$`;
const expected = /*html*/`<span class="math-tex">\\[
\\sqrt{x^{2}+1} \\
+ \\frac{1}{2}
\\]</span>`;
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
it("ignores math formulas inside code blocks and converts inline math expressions correctly", () => {
const result = markdownService.renderToHtml(trimIndentation`\
\`\`\`unknownlanguage
$$a+b$$
\`\`\`
`, "title");
expect(result).toBe(trimIndentation`\
<pre><code class="language-text-x-trilium-auto">$$a+b$$</code></pre>`);
});
it("converts specific inline math expression into Mathtex format", () => {
const input = `This is a formula: $\\mathcal{L}_{task} + \\mathcal{L}_{od}$ inside a sentence.`;
const expected = /*html*/`<p>This is a formula: <span class="math-tex">\\(\\mathcal{L}_{task} + \\mathcal{L}_{od}\\)</span> inside a sentence.</p>`;
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
it("converts math expressions inside list items into Mathtex format", () => {
const input = `- First item with formula: $E = mc^2$`;
const expected = /*html*/`<ul><li>First item with formula: <span class="math-tex">\\(E = mc^2\\)</span></li></ul>`;
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
it("converts display math expressions into Mathtex format", () => {
const input = `$$\sqrt{x^{2}+1}$$`;
const expected = /*html*/`<p><span class="math-tex">\\[\sqrt{x^{2}+1}\\]</span></p>`;
const expected = /*html*/`<span class="math-tex">\\[\sqrt{x^{2}+1}\\]</span>`;
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
@@ -240,7 +274,7 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li
});
it("imports todo lists properly", () => {
const input = trimIndentation`\
const input = trimIndentation`\
- [x] Hello
- [ ] World`;
const expected = `<ul class="todo-list"><li><label class="todo-list__label"><input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">Hello</span></label></li><li><label class="todo-list__label"><input type="checkbox" disabled="disabled"><span class="todo-list__label__description">World</span></label></li></ul>`;

View File

@@ -23,19 +23,7 @@ class CustomMarkdownRenderer extends Renderer {
}
paragraph(data: Tokens.Paragraph): string {
let text = super.paragraph(data).trimEnd();
if (text.includes("$")) {
// Display math
text = text.replaceAll(/(?<!\\)\$\$(.+)\$\$/g,
`<span class="math-tex">\\\[$1\\\]</span>`);
// Inline math
text = text.replaceAll(/(?<!\\)\$(.+?)\$/g,
`<span class="math-tex">\\\($1\\\)</span>`);
}
return text;
return super.paragraph(data).trimEnd();
}
code({ text, lang }: Tokens.Code): string {
@@ -133,11 +121,17 @@ function renderToHtml(content: string, title: string) {
// Double-escape slashes in math expression because they are otherwise consumed by the parser somewhere.
content = content.replaceAll("\\$", "\\\\$");
let html = parse(content, {
// Extract formulas and replace them with placeholders to prevent interference from Markdown rendering
const { processedText, placeholderMap: formulaMap } = extractFormulas(content);
let html = parse(processedText, {
async: false,
renderer: renderer
}) as string;
// After rendering, replace placeholders back with the formula HTML
html = restoreFromMap(html, formulaMap);
// h1 handling needs to come before sanitization
html = importUtils.handleH1(html, title);
html = htmlSanitizer.sanitize(html);
@@ -165,6 +159,59 @@ function getNormalizedMimeFromMarkdownLanguage(language: string | undefined) {
return MIME_TYPE_AUTO;
}
function extractCodeBlocks(text: string): { processedText: string, placeholderMap: Map<string, string> } {
const codeMap = new Map<string, string>();
let id = 0;
const timestamp = Date.now();
// Multi-line code block and Inline code
text = text.replace(/```[\s\S]*?```/g, (m) => {
const key = `<!--CODE_BLOCK_${timestamp}_${id++}-->`;
codeMap.set(key, m);
return key;
}).replace(/`[^`\n]+`/g, (m) => {
const key = `<!--INLINE_CODE_${timestamp}_${id++}-->`;
codeMap.set(key, m);
return key;
});
return { processedText: text, placeholderMap: codeMap };
}
function extractFormulas(text: string): { processedText: string, placeholderMap: Map<string, string> } {
// Protect the $ signs inside code blocks from being recognized as formulas.
const { processedText: noCodeText, placeholderMap: codeMap } = extractCodeBlocks(text);
const formulaMap = new Map<string, string>();
let id = 0;
const timestamp = Date.now();
// Display math and Inline math
let processedText = noCodeText.replace(/(?<!\\)\$\$((?:(?!\n{2,})[\s\S])+?)\$\$/g, (_, formula) => {
const key = `<!--FORMULA_BLOCK_${timestamp}_${id++}-->`;
const rendered = `<span class="math-tex">\\[${formula}\\]</span>`;
formulaMap.set(key, rendered);
return key;
}).replace(/(?<!\\)\$(.+?)\$/g, (_, formula) => {
const key = `<!--FORMULA_INLINE_${timestamp}_${id++}-->`;
const rendered = `<span class="math-tex">\\(${formula}\\)</span>`;
formulaMap.set(key, rendered);
return key;
});
processedText = restoreFromMap(processedText, codeMap);
return { processedText, placeholderMap: formulaMap };
}
function restoreFromMap(text: string, map: Map<string, string>): string {
if (map.size === 0) return text;
const pattern = [...map.keys()]
.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('|');
return text.replace(new RegExp(pattern, 'g'), match => map.get(match) ?? match);
}
const renderer = new CustomMarkdownRenderer({ async: false });
export default {

View File

@@ -26,7 +26,7 @@ import type {
*
* Manages and provides access to all available agent tools.
*/
class AgentToolsManager {
export class AgentToolsManager {
private vectorSearchTool: VectorSearchTool | null = null;
private noteNavigatorTool: NoteNavigatorTool | null = null;
private queryDecompositionTool: QueryDecompositionTool | null = null;

View File

@@ -16,14 +16,12 @@ import vectorStore from "./embeddings/index.js";
import providerManager from "./providers/providers.js";
import { ContextExtractor } from "./context/index.js";
import eventService from "../events.js";
import type { NoteEmbeddingContext } from "./embeddings/embeddings_interface.js";
import type { OptionDefinitions } from "@triliumnext/commons";
import sql from "../sql.js";
import sqlInit from "../sql_init.js";
import { CONTEXT_PROMPTS } from './constants/llm_prompt_constants.js';
import { SEARCH_CONSTANTS } from './constants/search_constants.js';
class IndexService {
export class IndexService {
private initialized = false;
private indexingInProgress = false;
private contextExtractor = new ContextExtractor();

View File

@@ -1,6 +1,6 @@
import backupService from "./backup.js";
import sql from "./sql.js";
import fs from "fs-extra";
import fs from "fs";
import log from "./log.js";
import { crash } from "./utils.js";
import resourceDir from "./resource_dir.js";

View File

@@ -207,7 +207,7 @@ async function getClient(opts: ClientOpts): Promise<Client> {
// it's not clear how to explicitly configure proxy (as opposed to system proxy),
// so in that case, we always use node's modules
if (isElectron && !opts.proxy) {
return (await import("electron")).net as Client;
return (await import("electron")).net as unknown as Client;
} else {
const { protocol } = url.parse(opts.url);

View File

@@ -5,7 +5,7 @@ import becca from "../becca/becca.js";
import type BNote from "../becca/entities/bnote.js";
import type { ApiParams } from "./backend_script_api_interface.js";
interface Bundle {
export interface Bundle {
note?: BNote;
noteId?: string;
script: string;

View File

@@ -8,7 +8,7 @@ import log from "./log.js";
import type { Statement, Database as DatabaseType, RunResult } from "better-sqlite3";
import dataDir from "./data_dir.js";
import cls from "./cls.js";
import fs from "fs-extra";
import fs from "fs";
import Database from "better-sqlite3";
import ws from "./ws.js";
import becca_loader from "../becca/becca_loader.js";
@@ -352,7 +352,6 @@ function disableSlowQueryLogging<T>(cb: () => T) {
}
export default {
dbConnection,
insert,
replace,

View File

@@ -1,7 +1,7 @@
import { BrowserWindow, Menu, Tray, ipcMain, nativeTheme } from "electron";
import electron from "electron";
import type { BrowserWindow, Tray } from "electron";
import { default as i18next, t } from "i18next";
import path from "path";
import { fileURLToPath } from "url";
import becca from "../becca/becca.js";
import becca_service from "../becca/becca_service.js";
@@ -33,7 +33,7 @@ function getTrayIconPath() {
}
function getIconPath(name: string) {
const suffix = !isMac && nativeTheme.shouldUseDarkColors ? "-inverted" : "";
const suffix = !isMac && electron.nativeTheme.shouldUseDarkColors ? "-inverted" : "";
return path.resolve(path.join(getResourceDir(), "assets", "images", "tray", `${name}Template${suffix}.png`));
}
@@ -216,7 +216,7 @@ function updateTrayMenu() {
}
const contextMenu = Menu.buildFromTemplate([
const contextMenu = electron.Menu.buildFromTemplate([
...windowVisibilityMenuItems,
{ type: "separator" },
{
@@ -255,7 +255,7 @@ function updateTrayMenu() {
type: "normal",
icon: getIconPath("close"),
click: () => {
const windows = BrowserWindow.getAllWindows();
const windows = electron.BrowserWindow.getAllWindows();
windows.forEach(window => {
window.close();
});
@@ -287,7 +287,7 @@ function createTray() {
return;
}
tray = new Tray(getTrayIconPath());
tray = new electron.Tray(getTrayIconPath());
tray.setToolTip(t("tray.tooltip"));
// Restore focus
tray.on("click", changeVisibility);
@@ -295,9 +295,9 @@ function createTray() {
if (!isMac) {
// macOS uses template icons which work great on dark & light themes.
nativeTheme.on("updated", updateTrayMenu);
electron.nativeTheme.on("updated", updateTrayMenu);
}
ipcMain.on("reload-tray", updateTrayMenu);
electron.ipcMain.on("reload-tray", updateTrayMenu);
i18next.on("languageChanged", updateTrayMenu);
}

View File

@@ -7,7 +7,7 @@ import entityChangesService from "./entity_changes.js";
import becca from "../becca/becca.js";
import type BNote from "../becca/entities/bnote.js";
interface ValidationResponse {
export interface ValidationResponse {
branch: BBranch | null;
success: boolean;
message?: string;

View File

@@ -9,8 +9,6 @@ import escape from "escape-html";
import sanitize from "sanitize-filename";
import mimeTypes from "mime-types";
import path from "path";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import type NoteMeta from "./meta/note_meta.js";
import log from "./log.js";
import { t } from "i18next";
@@ -226,7 +224,7 @@ export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?
});
}
interface DeferredPromise<T> extends Promise<T> {
export interface DeferredPromise<T> extends Promise<T> {
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
@@ -299,7 +297,7 @@ export function getResourceDir() {
return path.dirname(process.argv[1]);
}
return join(dirname(fileURLToPath(import.meta.url)), "..");
return path.join(__dirname, "..");
}
// TODO: Deduplicate with src/public/app/services/utils.ts

View File

@@ -7,14 +7,11 @@ import log from "./log.js";
import sqlInit from "./sql_init.js";
import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js";
import * as remoteMain from "@electron/remote/main";
import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron";
import { dialog, ipcMain } from "electron";
import electron from "electron";
import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents } from "electron";
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { t } from "i18next";
import { RESOURCE_DIR } from "./resource_dir.js";
// Prevent the window being garbage collected
let mainWindow: BrowserWindow | null;
@@ -28,14 +25,14 @@ function trackWindowFocus(win: BrowserWindow) {
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
allWindows.push(win);
if (!optionService.getOptionBool("disableTray")) {
ipcMain.emit("reload-tray");
electron.ipcMain.emit("reload-tray");
}
});
win.on("closed", () => {
allWindows = allWindows.filter(w => !w.isDestroyed());
if (!optionService.getOptionBool("disableTray")) {
ipcMain.emit("reload-tray");
electron.ipcMain.emit("reload-tray");
}
});
}
@@ -66,7 +63,7 @@ async function createExtraWindow(extraWindowHash: string) {
trackWindowFocus(win);
}
ipcMain.on("create-extra-window", (event, arg) => {
electron.ipcMain.on("create-extra-window", (event, arg) => {
createExtraWindow(arg.extraWindowHash);
});
@@ -76,13 +73,13 @@ interface ExportAsPdfOpts {
pageSize: "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Legal" | "Letter" | "Tabloid" | "Ledger";
}
ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
const browserWindow = BrowserWindow.fromWebContents(e.sender);
electron.ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
const browserWindow = electron.BrowserWindow.fromWebContents(e.sender);
if (!browserWindow) {
return;
}
const filePath = dialog.showSaveDialogSync(browserWindow, {
const filePath = electron.dialog.showSaveDialogSync(browserWindow, {
defaultPath: formatDownloadTitle(opts.title, "file", "application/pdf"),
filters: [
{
@@ -111,18 +108,18 @@ ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
`
});
} catch (e) {
dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
return;
}
try {
await fs.writeFile(filePath, buffer);
} catch (e) {
dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
return;
}
shell.openPath(filePath);
electron.shell.openPath(filePath);
});
async function createMainWindow(app: App) {
@@ -226,7 +223,8 @@ function getWindowExtraOpts() {
return extraOpts;
}
function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
async function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
const remoteMain = (await import("@electron/remote/main/index.js")).default;
remoteMain.enable(webContents);
webContents.setWindowOpenHandler((details) => {
@@ -259,7 +257,7 @@ function configureWebContents(webContents: WebContents, spellcheckEnabled: boole
}
function getIcon() {
return path.join(dirname(fileURLToPath(import.meta.url)), "../../images/app-icons/png/256x256" + (isDev ? "-dev" : "") + ".png");
return path.join(RESOURCE_DIR, "images/app-icons/png/256x256" + (isDev ? "-dev" : "") + ".png");
}
async function createSetupWindow() {

View File

@@ -195,9 +195,11 @@ function register(router: Router) {
try {
const content = templateNote.getContent();
if (typeof content === "string") {
const ejsResult = ejs.render(content, opts, { includer });
res.send(ejsResult);
useDefaultView = false; // Rendering went okay, don't use default view
import("ejs").then((ejs) => {
const ejsResult = ejs.render(content, opts, { includer });
res.send(ejsResult);
useDefaultView = false; // Rendering went okay, don't use default view
});
}
} catch (e: unknown) {
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);