Merge branch 'main' into feat/extra-window
Some checks failed
Checks / main (push) Has been cancelled

This commit is contained in:
SiriusXT 2026-01-05 11:28:47 +08:00
commit 80404b83b0
37 changed files with 432 additions and 2128 deletions

1
.gitignore vendored
View File

@ -51,3 +51,4 @@ upload
# docs
site/
apps/*/coverage
scripts/translation/.language*.json

View File

@ -194,7 +194,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
if (type === "pdf") {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
$content.append($pdfPreview);
} else if (type === "audio") {
@ -218,14 +218,14 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
// in attachment list
const $downloadButton = $(`
<button class="file-download btn btn-primary" type="button">
<span class="bx bx-download"></span>
<span class="tn-icon bx bx-download"></span>
${t("file_properties.download")}
</button>
`);
const $openButton = $(`
<button class="file-open btn btn-primary" type="button">
<span class="bx bx-link-external"></span>
<span class="tn-icon bx bx-link-external"></span>
${t("file_properties.open")}
</button>
`);

View File

@ -1596,7 +1596,9 @@
"toggle-sidebar": "切换侧边栏",
"dropping-not-allowed": "不允许移动笔记到此处。",
"shared-indicator-tooltip": "此笔记已公开分享",
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}"
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}",
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
"clone-indicator-tooltip-single": "此笔记已克隆1 个额外的父级:{{- parent}}"
},
"title_bar_buttons": {
"window-on-top": "保持此窗口置顶"
@ -2194,7 +2196,14 @@
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。",
"shared_copy_to_clipboard": "复制链接到剪贴板",
"shared_open_in_browser": "在浏览器中打开链接",
"shared_unshare": "取消共享"
"shared_unshare": "取消共享",
"save_status_saved": "已保存",
"save_status_saving": "保存中...",
"save_status_unsaved": "未保存",
"save_status_error": "保存失败",
"save_status_unsaved_tooltip": "还有一些更改尚未保存。它们将稍后自动保存。",
"save_status_error_tooltip": "保存笔记时出错。如果可以,请尝试将笔记内容复制到其他位置并重新加载应用程序。",
"save_status_saving_tooltip": "更改正在保存。"
},
"status_bar": {
"language_title": "更改内容语言",

View File

@ -2196,7 +2196,14 @@
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。",
"shared_copy_to_clipboard": "リンクをクリップボードにコピー",
"shared_open_in_browser": "ブラウザでリンクを開く",
"shared_unshare": "共有を削除"
"shared_unshare": "共有を削除",
"save_status_saved": "保存されました",
"save_status_saving": "保存中...",
"save_status_unsaved": "未保存",
"save_status_error": "保存に失敗しました",
"save_status_saving_tooltip": "変更を保存しています。",
"save_status_unsaved_tooltip": "未保存の変更があります。すぐに自動的に保存されます。",
"save_status_error_tooltip": "ノートの保存中にエラーが発生しました。可能であれば、ノートの内容を別の場所にコピーして、アプリケーションを再読み込みしてください。"
},
"status_bar": {
"language_title": "コンテンツの言語を変更",

View File

@ -2200,7 +2200,14 @@
"read_only_temporarily_disabled_description": "此筆記目前可編輯,但通常為唯讀狀態。當您切換至其他筆記時,本筆記將立即恢復為唯讀模式。\n\n點擊此處重新啟用唯讀模式。",
"clipped_note_description": "本筆記原始來源為 {{url}}。\n\n點擊此處前往原網頁。",
"execute_script_description": "此筆記為腳本筆記。點擊以執行腳本。",
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。"
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。",
"save_status_saved": "已儲存",
"save_status_saving": "正在儲存…",
"save_status_unsaved": "未儲存",
"save_status_error": "儲存失敗",
"save_status_saving_tooltip": "正在儲存更動。",
"save_status_unsaved_tooltip": "仍有更動尚未儲存。它們將在稍後自動儲存。",
"save_status_error_tooltip": "在儲存筆記時發生錯誤。如果可以,請嘗試將筆記內容複製至他處並重新載入應用程式。"
},
"breadcrumb": {
"hoisted_badge": "聚焦",
@ -2246,6 +2253,6 @@
"pages_one": "共 {{count}} 頁",
"pages_other": "",
"pages_alt": "第 {{pageNumber}} 頁",
"pages_loading": "載入…"
"pages_loading": "正在載入…"
}
}

View File

@ -45,6 +45,10 @@ interface WithContext {
interface PdfDocumentModifiedMessage extends WithContext {
type: "pdfjs-viewer-document-modified";
}
interface PdfDocumentBlobResultMessage extends WithContext {
type: "pdfjs-viewer-blob";
data: Uint8Array<ArrayBufferLike>;
}
@ -113,4 +117,5 @@ type PdfMessageEvent = MessageEvent<
| PdfViewerThumbnailMessage
| PdfViewerAttachmentsMessage
| PdfViewerLayersMessage
| PdfDocumentBlobResultMessage
>;

View File

@ -1,27 +1,29 @@
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
import { ViewModeProps } from "../interface";
import Calendar from "./calendar";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import "./index.css";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { Calendar as FullCalendar } from "@fullcalendar/core";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
import dialog from "../../../services/dialog";
import { t } from "../../../services/i18n";
import { buildEvents, buildEventsForCalendar } from "./event_builder";
import { changeEvent, newEvent } from "./api";
import froca from "../../../services/froca";
import date_notes from "../../../services/date_notes";
import appContext from "../../../components/app_context";
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
import { DateClickArg } from "@fullcalendar/interaction";
import FNote from "../../../entities/fnote";
import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { RefObject } from "preact";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { openCalendarContextMenu } from "./context_menu";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
import date_notes from "../../../services/date_notes";
import dialog from "../../../services/dialog";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
import { isMobile } from "../../../services/utils";
import ActionButton from "../../react/ActionButton";
import Button, { ButtonGroup } from "../../react/Button";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { ViewModeProps } from "../interface";
import { changeEvent, newEvent } from "./api";
import Calendar from "./calendar";
import { openCalendarContextMenu } from "./context_menu";
import { buildEvents, buildEventsForCalendar } from "./event_builder";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
interface CalendarViewData {
@ -59,7 +61,7 @@ const CALENDAR_VIEWS = [
previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next")
}
]
];
const SUPPORTED_CALENDAR_VIEW_TYPE = CALENDAR_VIEWS.map(v => v.type);
@ -75,6 +77,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
ru: () => import("@fullcalendar/core/locales/ru"),
ja: () => import("@fullcalendar/core/locales/ja"),
pt: () => import("@fullcalendar/core/locales/pt"),
pl: () => import("@fullcalendar/core/locales/pl"),
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
uk: () => import("@fullcalendar/core/locales/uk"),
en: null,
@ -102,9 +105,9 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const eventBuilder = useMemo(() => {
if (!isCalendarRoot) {
return async () => await buildEvents(noteIds);
} else {
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
}
}
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
}, [isCalendarRoot, noteIds]);
const plugins = usePlugins(isEditable, isCalendarRoot);
@ -178,7 +181,7 @@ function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar>
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
</ButtonGroup>
</div>
)
);
}
function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
@ -293,7 +296,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
if (promotedAttributes) {
let promotedAttributesHtml = "";
for (const [name, value] of promotedAttributes) {
promotedAttributesHtml = promotedAttributesHtml + /*html*/`\
promotedAttributesHtml = `${promotedAttributesHtml /*html*/}\
<div class="promoted-attribute">
<span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span>
</div>`;

View File

@ -142,4 +142,10 @@
border: 1px solid var(--main-border-color);
background: var(--more-accented-background-color);
}
.note-list.grid-view .note-path {
margin-left: 0.5em;
vertical-align: middle;
opacity: 0.5;
}
/* #endregion */

View File

@ -7,7 +7,6 @@ import attribute_renderer from "../../../services/attribute_renderer";
import content_renderer from "../../../services/content_renderer";
import { t } from "../../../services/i18n";
import link from "../../../services/link";
import tree from "../../../services/tree";
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
import Icon from "../../react/Icon";
import NoteLink from "../../react/NoteLink";
@ -103,16 +102,7 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
}
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
const titleRef = useRef<HTMLSpanElement>(null);
const [ noteTitle, setNoteTitle ] = useState<string>();
const notePath = getNotePath(parentNote, note);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => {
tree.getNoteTitle(note.noteId, parentNote.noteId).then(setNoteTitle);
}, [ note ]);
useEffect(() => highlightSearch(titleRef.current), [ noteTitle, highlightedTokens ]);
return (
<div
@ -123,7 +113,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
>
<h5 className="note-book-header">
<Icon className="note-icon" icon={note.getIcon()} />
<span ref={titleRef} className="note-book-title">{noteTitle}</span>
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={parentNote.type === "search"} highlightedTokens={highlightedTokens} />
<NoteAttributes note={note} />
</h5>
<NoteContent

View File

@ -170,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
return spacedUpdate;
}
export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval, replaceWithoutRevision }: {
noteType: NoteType;
note: FNote,
noteContext: NoteContext | null | undefined,
getData: () => Promise<Blob | undefined> | Blob | undefined,
onContentChange: (newBlob: FBlob) => void,
dataSaved?: (savedData: Blob) => void,
updateInterval?: number;
/** If set to true, then the blob is replaced directly without saving a revision before. */
replaceWithoutRevision?: boolean;
}) {
const parentComponent = useContext(ParentComponent);
const blob = useNoteBlob(note, parentComponent?.componentId);
const callback = useMemo(() => {
return async () => {
const data = await getData();
// for read only notes
if (data === undefined || note.type !== noteType) return;
protected_session_holder.touchProtectedSessionIfNecessary(note);
await server.upload(`notes/${note.noteId}/file?replace=${replaceWithoutRevision ? "1" : "0"}`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId);
dataSaved?.(data);
};
}, [ note, getData, dataSaved, noteType, parentComponent, replaceWithoutRevision ]);
const stateCallback = useCallback<StateCallback>((state) => {
noteContext?.setContextData("saveState", {
state
});
}, [ noteContext ]);
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
// React to note/blob changes.
useEffect(() => {
if (!blob) return;
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob));
}, [ blob ]);
// React to update interval changes.
useEffect(() => {
if (!updateInterval) return;
spacedUpdate.setUpdateInterval(updateInterval);
}, [ updateInterval ]);
// Save if needed upon switching tabs.
useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => {
if (eventNoteContext.ntxId !== noteContext?.ntxId) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon tab closing.
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon window/browser closing.
useEffect(() => {
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
appContext.addBeforeUnloadListener(listener);
return () => appContext.removeBeforeUnloadListener(listener);
}, []);
return spacedUpdate;
}
export function useNoteSavedData(noteId: string | undefined) {
return useSyncExternalStore(
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},

View File

@ -37,6 +37,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
it: "it",
ja: "ja",
pt: "pt",
pl: null,
pt_br: "pt",
ro: "ro",
ru: "ru",

View File

@ -13,6 +13,7 @@ export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"]
it: "it-IT",
ja: "ja-JP",
pt: "pt-PT",
pl: "pl-PL",
pt_br: "pt-BR",
ro: "ro-RO",
ru: "ru-RU",

View File

@ -4,9 +4,8 @@ import appContext from "../../../components/app_context";
import type NoteContext from "../../../components/note_context";
import FBlob from "../../../entities/fblob";
import FNote from "../../../entities/fnote";
import server from "../../../services/server";
import { useViewModeConfig } from "../../collections/NoteList";
import { useTriliumEvent } from "../../react/hooks";
import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import PdfViewer from "./PdfViewer";
export default function PdfPreview({ note, blob, componentId, noteContext }: {
@ -18,12 +17,48 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
const iframeRef = useRef<HTMLIFrameElement>(null);
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory");
const spacedUpdate = useBlobEditorSpacedUpdate({
note,
noteType: "file",
noteContext,
getData() {
if (!iframeRef.current?.contentWindow) return undefined;
return new Promise<Blob>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Timeout while waiting for blob response"));
}, 10_000);
const onMessageReceived = (event: PdfMessageEvent) => {
if (event.data.type !== "pdfjs-viewer-blob") return;
if (event.data.noteId !== note.noteId || event.data.ntxId !== noteContext.ntxId) return;
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
clearTimeout(timeout);
window.removeEventListener("message", onMessageReceived);
resolve(blob);
};
window.addEventListener("message", onMessageReceived);
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-blob",
}, window.location.origin);
});
},
onContentChange() {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
},
replaceWithoutRevision: true
});
useEffect(() => {
function handleMessage(event: PdfMessageEvent) {
if (event.data?.type === "pdfjs-viewer-document-modified") {
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
server.upload(`notes/${note.noteId}/file`, new File([blob], note.title, { type: note.mime }), componentId);
spacedUpdate.resetUpdateTimer();
spacedUpdate.scheduleUpdate();
}
}
@ -138,13 +173,6 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
};
}, [ note, historyConfig, componentId, blob, noteContext ]);
// Refresh when blob changes.
useEffect(() => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
}, [ blob ]);
useTriliumEvent("customDownload", ({ ntxId }) => {
if (ntxId !== noteContext.ntxId) return;
iframeRef.current?.contentWindow?.postMessage({
@ -171,6 +199,7 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
});
}
}}
editable
/>
);
}

View File

@ -15,12 +15,16 @@ interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabInd
/** Note: URLs are relative to /pdfjs/web. */
pdfUrl: string;
onLoad?(): void;
/**
* If set, enables editable mode which includes persistence of user settings, annotations as well as specific features such as sending table of contents data for the sidebar.
*/
editable?: boolean;
}
/**
* Reusable component displaying a PDF. The PDF needs to be provided via a URL.
*/
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad }: PdfViewerProps) {
export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad, editable }: PdfViewerProps) {
const iframeRef = useSyncedRef(externalIframeRef, null);
const [ locale ] = useTriliumOption("locale");
const [ newLayout ] = useTriliumOptionBool("newLayout");
@ -30,7 +34,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad
<iframe
ref={iframeRef}
class="pdf-preview"
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}`}
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
onLoad={() => {
injectStyles();
onLoad?.();

View File

@ -70,6 +70,20 @@ test("Attachments listing works", async ({ page, context }) => {
await expect(attachmentsList.locator(".pdf-attachment-item")).toHaveCount(0);
});
test("Download original PDF works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Dacia Logan.pdf");
const pdfHelper = new PdfHelper(app);
await pdfHelper.toBeInitialized();
const [ download ] = await Promise.all([
page.waitForEvent("download"),
app.currentNoteSplit.locator(".icon-action.bx.bx-download").click()
]);
expect(download).toBeDefined();
});
test("Layers listing works", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
@ -108,4 +122,8 @@ class PdfHelper {
async expectPageToBe(expectedPageNumber: number) {
await expect(this.contentFrame.locator("#pageNumber")).toHaveValue(`${expectedPageNumber}`);
}
async toBeInitialized() {
await expect(this.contentFrame.locator("#pageNumber")).toBeVisible();
}
}

View File

@ -9,7 +9,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://host.docker.internal:8082; # change it to a different port if non-default is used
proxy_pass http://127.0.0.1:8082;
proxy_cookie_path / /trilium/;
proxy_read_timeout 90;
}

View File

@ -25,7 +25,8 @@
"docker-start-rootless-debian": "pnpm docker-build-rootless-debian && docker run -p 8081:8080 triliumnext-rootless-debian",
"docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine",
"generate-document": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx ./scripts/generate_document.ts",
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest"
"proxy-traefik": "docker run --name trilium-traefik --rm --network=host -v ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml:ro -v ./docker/traefik/dynamic:/etc/traefik/dynamic traefik:latest",
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
},
"dependencies": {
"better-sqlite3": "12.5.0",

View File

@ -28,7 +28,9 @@ function updateFile(req: Request) {
};
}
note.saveRevision();
if (req.query.replace !== "1") {
note.saveRevision();
}
note.mime = file.mimetype.toLowerCase();
note.save();

View File

@ -3,7 +3,8 @@
"title": "Kom i gang",
"architecture": "Arkitektur:",
"desktop_title": "Last ned skrivebordsprogram (v{{version}})",
"older_releases": "Se tidligere versjoner"
"older_releases": "Se tidligere versjoner",
"server_title": "Sett opp en server for adgang fra flere enheter"
},
"hero_section": {
"title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.",
@ -13,14 +14,19 @@
},
"organization_benefits": {
"title": "Organisering",
"note_structure_title": "Notatstruktur"
"note_structure_title": "Notatstruktur",
"hoisting_title": "Arbeidsflate og fokusering",
"attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler."
},
"productivity_benefits": {
"sync_title": "Synkronisering",
"search_title": "Kraftig søk",
"web_clipper_title": "Web clipper",
"revisions_title": "Notatrevisjon",
"protected_notes_title": "Beskyttede notater"
"protected_notes_title": "Beskyttede notater",
"title": "Produktivitet og sikkerhet",
"sync_content": "Bruk en selv-hostet eller cloud-instans for å enkelt synkronisere notater på tvers av enheter, og ha de tilgjengelige fra din mobiltelefon ved hjelp av progressiv web-app.",
"jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser."
},
"note_types": {
"canvas_title": "Kanvas",
@ -33,7 +39,8 @@
"extensibility_benefits": {
"import_export_title": "Import/eksport",
"scripting_title": "Avansert skripting",
"api_title": "REST API"
"api_title": "REST API",
"title": "Deling og utvidbarhet"
},
"collections": {
"title": "Samlinger",
@ -41,7 +48,8 @@
"table_title": "Tabell",
"geomap_title": "Geokart",
"presentation_title": "Presentasjon",
"board_title": "Kanbantavle"
"board_title": "Kanbantavle",
"geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din."
},
"header": {
"documentation": "Dokumentasjon",
@ -58,12 +66,15 @@
"paypal": "PayPal",
"title": "Støtt oss",
"financial_donations_title": "Finansiell donasjon",
"github_sponsors": "GitHub Sponsors"
"github_sponsors": "GitHub Sponsors",
"financial_donations_description": "Trilium er bygget og vedlikeholdt med <Link>flere hundre timers arbeid</Link>. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader."
},
"download_helper_desktop_windows": {
"download_scoop": "Scoop",
"title_x64": "Windows 64-bit",
"download_zip": "Portable (.zip)"
"download_zip": "Portable (.zip)",
"title_arm64": "Windows på ARM",
"download_exe": "Last ned installasjonsprogram (.exe)"
},
"download_helper_desktop_linux": {
"download_deb": ".deb",
@ -72,15 +83,19 @@
"download_nixpkgs": "nixpkgs",
"download_aur": "AUR",
"title_x64": "Linux 64-bit",
"download_zip": "Portable (.zip)"
"download_zip": "Portable (.zip)",
"title_arm64": "Linux på ARM"
},
"download_helper_server_docker": {
"download_ghcr": "ghcr.io",
"download_dockerhub": "Docker Hub"
"download_dockerhub": "Docker Hub",
"title": "Selv-hostet med Docker"
},
"download_helper_desktop_macos": {
"download_homebrew_cask": "Homebrew Cask",
"download_zip": "Portable (.zip)"
"download_zip": "Portable (.zip)",
"title_x64": "macOS for Intel",
"download_dmg": "Last ned installasjonsprogram (.dmg)"
},
"final_cta": {
"get_started": "Kom i gang"
@ -91,7 +106,9 @@
"download_now": {
"text": "Last ned nå ",
"platform_small": "for {{platform}}",
"linux_small": "for Linux"
"linux_small": "for Linux",
"platform_big": "v{{version}} for {{platform}}",
"linux_big": "v{{version}} for Linux"
},
"footer": {
"copyright_and_the": " og ",
@ -100,9 +117,17 @@
"download_helper_server_linux": {
"download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.tar.xz)",
"download_nixos": "NixOS modul"
"download_nixos": "NixOS modul",
"title": "Selv-hostet på Linux"
},
"download_helper_server_hosted": {
"title": "Betalt hosting"
"title": "Betalt hosting",
"download_triliumcc": "Alternativt sjekk trilium.cc"
},
"faq": {
"title": "Ofte stilte spørsmål"
},
"404": {
"title": "404: Siden ble ikke funnet"
}
}

View File

@ -10201,6 +10201,13 @@
"value": "bx bxs-file-pdf",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "shareAlias",
"value": "pdf",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",

View File

@ -39,7 +39,7 @@
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.16",
"webdriverio": "9.22.0"
"webdriverio": "9.23.0"
},
"peerDependencies": {
"ckeditor5": "47.3.0"

View File

@ -40,7 +40,7 @@
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.16",
"webdriverio": "9.22.0"
"webdriverio": "9.23.0"
},
"peerDependencies": {
"ckeditor5": "47.3.0"

View File

@ -42,7 +42,7 @@
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.16",
"webdriverio": "9.22.0"
"webdriverio": "9.23.0"
},
"peerDependencies": {
"ckeditor5": "47.3.0"

View File

@ -42,7 +42,7 @@
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.16",
"webdriverio": "9.22.0"
"webdriverio": "9.23.0"
},
"peerDependencies": {
"ckeditor5": "47.3.0"

View File

@ -42,7 +42,7 @@
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.16",
"webdriverio": "9.22.0"
"webdriverio": "9.23.0"
},
"peerDependencies": {
"ckeditor5": "47.3.0"

View File

@ -47,6 +47,7 @@ export const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs
"ku": () => import("dayjs/locale/ku.js"),
"pt_br": () => import("dayjs/locale/pt-br.js"),
"pt": () => import("dayjs/locale/pt.js"),
"pl": () => import("dayjs/locale/pl.js"),
"ro": () => import("dayjs/locale/ro.js"),
"ru": () => import("dayjs/locale/ru.js"),
"tw": () => import("dayjs/locale/zh-tw.js"),

View File

@ -23,6 +23,7 @@ const UNSORTED_LOCALES = [
{ id: "ja", name: "日本語", electronLocale: "ja" },
{ id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR" },
{ id: "pt", name: "Português (Portugal)", electronLocale: "pt_PT" },
{ id: "pl", name: "Polski", electronLocale: "pl" },
{ id: "ro", name: "Română", electronLocale: "ro" },
{ id: "ru", name: "Русский", electronLocale: "ru" },
{ id: "tw", name: "繁體中文", electronLocale: "zh_TW" },

View File

@ -3,6 +3,8 @@ import BuildHelper from "../../../scripts/build-utils";
import { build as esbuild } from "esbuild";
import { LOCALES } from "@triliumnext/commons";
import { watch } from "chokidar";
import { readFileSync, writeFileSync } from "fs";
import packageJson from "../package.json" with { type: "json " };
const build = new BuildHelper("packages/pdfjs-viewer");
const watchMode = process.argv.includes("--watch");
@ -16,6 +18,7 @@ async function main() {
for (const file of [ "viewer.css", "viewer.html", "viewer.mjs" ]) {
build.copy(`viewer/${file}`, `web/${file}`);
}
patchCacheBuster(`${build.outDir}/web/viewer.html`);
build.copy(`viewer/images`, `web/images`);
// Copy the custom files.
@ -34,8 +37,9 @@ async function main() {
build.writeJson("web/locale/locale.json", localeMappings);
// Copy pdfjs-dist files.
build.copy("/node_modules/pdfjs-dist/build/pdf.mjs", "build/pdf.mjs");
build.copy("/node_modules/pdfjs-dist/build/pdf.worker.mjs", "build/pdf.worker.mjs");
for (const file of [ "pdf.mjs", "pdf.worker.mjs", "pdf.sandbox.mjs" ]) {
build.copy(join("/node_modules/pdfjs-dist/build", file), join("build", file));
}
if (watchMode) {
watchForChanges();
@ -59,6 +63,21 @@ async function rebuildCustomFiles() {
build.copy("src/custom.css", "web/custom.css");
}
function patchCacheBuster(htmlFilePath: string) {
const version = packageJson.version;
console.log(`Versioned URLs: ${version}.`)
let html = readFileSync(htmlFilePath, "utf-8");
html = html.replace(
`<link rel="stylesheet" href="custom.css" />`,
`<link rel="stylesheet" href="custom.css?v=${version}" />`);
html = html.replace(
`<script src="custom.mjs" type="module"></script>`,
`<script src="custom.mjs?v=${version}" type="module"></script>`
);
writeFileSync(htmlFilePath, html);
}
function watchForChanges() {
console.log("Watching for changes in src directory...");
const watcher = watch(join(build.projectDir, "src"), {

View File

@ -6,11 +6,14 @@ import { setupPdfLayers } from "./layers";
async function main() {
const urlParams = new URLSearchParams(window.location.search);
const isEditable = urlParams.get("editable") === "1";
if (urlParams.get("sidebar") === "0") {
hideSidebar();
}
interceptPersistence(getCustomAppOptions(urlParams));
if (isEditable) {
interceptPersistence(getCustomAppOptions(urlParams));
}
// Wait for the PDF viewer application to be available.
while (!window.PDFViewerApplication) {
@ -18,16 +21,18 @@ async function main() {
}
const app = window.PDFViewerApplication;
app.eventBus.on("documentloaded", () => {
manageSave();
manageDownload();
extractAndSendToc();
setupScrollToHeading();
setupActiveHeadingTracking();
setupPdfPages();
setupPdfAttachments();
setupPdfLayers();
});
if (isEditable) {
app.eventBus.on("documentloaded", () => {
manageSave();
manageDownload();
extractAndSendToc();
setupScrollToHeading();
setupActiveHeadingTracking();
setupPdfPages();
setupPdfAttachments();
setupPdfLayers();
});
}
await app.initializedPromise;
};
@ -55,37 +60,38 @@ function getCustomAppOptions(urlParams: URLSearchParams) {
function manageSave() {
const app = window.PDFViewerApplication;
const storage = app.pdfDocument.annotationStorage;
let timeout = null;
function debouncedSave() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(async () => {
if (!storage) return;
function onChange() {
if (!storage) return;
window.parent.postMessage({
type: "pdfjs-viewer-document-modified",
ntxId: window.TRILIUM_NTX_ID,
noteId: window.TRILIUM_NOTE_ID
} satisfies PdfDocumentModifiedMessage, window.location.origin);
storage.resetModified();
}
window.addEventListener("message", async (event) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type === "trilium-request-blob") {
const app = window.PDFViewerApplication;
const data = await app.pdfDocument.saveDocument();
window.parent.postMessage({
type: "pdfjs-viewer-document-modified",
type: "pdfjs-viewer-blob",
data,
ntxId: window.TRILIUM_NTX_ID,
noteId: window.TRILIUM_NOTE_ID
} satisfies PdfDocumentModifiedMessage, window.location.origin);
storage.resetModified();
timeout = null;
}, 2_000);
}
app.pdfDocument.annotationStorage.onSetModified = debouncedSave; // works great for most cases, including forms.
app.eventBus.on("annotationeditorcommit", debouncedSave);
app.eventBus.on("annotationeditorparamschanged", debouncedSave);
app.eventBus.on("annotationeditorstateschanged", evt => { // needed for detecting when annotations are moved around.
const { activeEditorId } = evt;
// When activeEditorId becomes null, an editor was just committed
if (activeEditorId === null) {
debouncedSave();
} satisfies PdfDocumentBlobResultMessage, window.location.origin);
}
});
app.pdfDocument.annotationStorage.onSetModified = () => {
onChange();
}; // works great for most cases, including forms.
app.eventBus.on("switchannotationeditorparams", () => {
onChange();
});
}
function manageDownload() {

68
pnpm-lock.yaml generated
View File

@ -54,7 +54,7 @@ importers:
version: 24.10.4
'@vitest/browser-webdriverio':
specifier: 4.0.16
version: 4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16)(webdriverio@9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))
version: 4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16)(webdriverio@9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/coverage-v8':
specifier: 4.0.16
version: 4.0.16(@vitest/browser@4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16))(vitest@4.0.16)
@ -945,8 +945,8 @@ importers:
specifier: 4.0.16
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-webdriverio@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
webdriverio:
specifier: 9.22.0
version: 9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
specifier: 9.23.0
version: 9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
packages/ckeditor5-footnotes:
devDependencies:
@ -1005,8 +1005,8 @@ importers:
specifier: 4.0.16
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-webdriverio@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
webdriverio:
specifier: 9.22.0
version: 9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
specifier: 9.23.0
version: 9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
packages/ckeditor5-keyboard-marker:
devDependencies:
@ -1065,8 +1065,8 @@ importers:
specifier: 4.0.16
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-webdriverio@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
webdriverio:
specifier: 9.22.0
version: 9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
specifier: 9.23.0
version: 9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
packages/ckeditor5-math:
dependencies:
@ -1129,8 +1129,8 @@ importers:
specifier: 4.0.16
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-webdriverio@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
webdriverio:
specifier: 9.22.0
version: 9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
specifier: 9.23.0
version: 9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
packages/ckeditor5-mermaid:
dependencies:
@ -1196,8 +1196,8 @@ importers:
specifier: 4.0.16
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-webdriverio@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
webdriverio:
specifier: 9.22.0
version: 9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
specifier: 9.23.0
version: 9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
packages/codemirror:
dependencies:
@ -6065,8 +6065,8 @@ packages:
'@vue/shared@3.5.14':
resolution: {integrity: sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==}
'@wdio/config@9.22.0':
resolution: {integrity: sha512-SQsTSZowEI+whPlwPLsX9ICr6BiG39NLmzED7OWfaowribQ0XylRhoWodcRu6cB/ZCzminZajBUG5XgarNWnRw==}
'@wdio/config@9.23.0':
resolution: {integrity: sha512-hhtngUG2uCxYmScSEor+k22EVlsTW3ARXgke8NPVeQA4p1+GC2CvRZi4P7nmhRTZubgLrENYYsveFcYR+1UXhQ==}
engines: {node: '>=18.20.0'}
'@wdio/logger@9.18.0':
@ -6084,8 +6084,8 @@ packages:
resolution: {integrity: sha512-zMmAtse2UMCSOW76mvK3OejauAdcFGuKopNRH7crI0gwKTZtvV89yXWRziz9cVXpFgfmJCjf9edxKFWdhuF5yw==}
engines: {node: '>=18.20.0'}
'@wdio/utils@9.22.0':
resolution: {integrity: sha512-5j2nn2bBjj41wxXsVT43sUMOKR0qiKNDRG1UcKQ6NkfsWFObSehMAS0a9ZZu//+ooTxRkwHjvLdQrXIrPnTLzg==}
'@wdio/utils@9.23.0':
resolution: {integrity: sha512-WhXuVSxEvPw/i34bL1aCHAOi+4g29kRkIMyBShNSxH+Shxh2G91RJYsXm4IAiPMGcC4H6G8T2VcbZ32qnGPm5Q==}
engines: {node: '>=18.20.0'}
'@webassemblyjs/ast@1.14.1':
@ -14150,12 +14150,12 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webdriver@9.22.0:
resolution: {integrity: sha512-jf4irPhIJAssrF3mqUrBZGZnzjRfM86Q24ePUOgFKWI04LtdvRsnc9SsWU05mrN/a6pTJzGps6GsvLpNhvcalg==}
webdriver@9.23.0:
resolution: {integrity: sha512-XkZOhjoBOY7maKI3BhDF2rNiDne4wBD6Gw6VUnt4X9b7j9NtfzcCrThBlT0hnA8W77bWNtMRCSpw9Ajy08HqKg==}
engines: {node: '>=18.20.0'}
webdriverio@9.22.0:
resolution: {integrity: sha512-sqXZG11hRM9KjqioVPcXCPLIcdJprNM9e+B6JlyacN6ImgC64MQbgs0vtCDLVsSIX7vg+x771lrS/VxXxqlkJw==}
webdriverio@9.23.0:
resolution: {integrity: sha512-Y5y4jpwHvuduUfup+gXTuCU6AROn/k6qOba3st0laFluKHY+q5SHOpQAJdS8acYLwE8caDQ2dXJhmXyxuJrm0Q==}
engines: {node: '>=18.20.0'}
peerDependencies:
puppeteer-core: '>=22.x || <=24.x'
@ -15361,6 +15361,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@47.3.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@ -16091,6 +16093,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-restricted-editing@47.3.0':
dependencies:
@ -20865,11 +20869,11 @@ snapshots:
- bufferutil
- utf-8-validate
'@vitest/browser-webdriverio@4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16)(webdriverio@9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))':
'@vitest/browser-webdriverio@4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16)(webdriverio@9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))':
dependencies:
'@vitest/browser': 4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/browser-webdriverio@4.0.16)(@vitest/ui@4.0.16)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
webdriverio: 9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
webdriverio: 9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
transitivePeerDependencies:
- bufferutil
- msw
@ -21027,11 +21031,11 @@ snapshots:
'@vue/shared@3.5.14': {}
'@wdio/config@9.22.0':
'@wdio/config@9.23.0':
dependencies:
'@wdio/logger': 9.18.0
'@wdio/types': 9.20.0
'@wdio/utils': 9.22.0
'@wdio/utils': 9.23.0
deepmerge-ts: 7.1.5
glob: 10.4.5
import-meta-resolve: 4.2.0
@ -21057,7 +21061,7 @@ snapshots:
dependencies:
'@types/node': 20.19.25
'@wdio/utils@9.22.0':
'@wdio/utils@9.23.0':
dependencies:
'@puppeteer/browsers': 2.10.10
'@wdio/logger': 9.18.0
@ -30844,7 +30848,7 @@ snapshots:
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/node': 24.10.4
'@vitest/browser-webdriverio': 4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16)(webdriverio@9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/browser-webdriverio': 4.0.16(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.16)(webdriverio@9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))
'@vitest/ui': 4.0.16(vitest@4.0.16)
happy-dom: 20.0.11
jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
@ -30934,15 +30938,15 @@ snapshots:
web-streams-polyfill@3.3.3: {}
webdriver@9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
webdriver@9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
dependencies:
'@types/node': 20.19.25
'@types/ws': 8.18.1
'@wdio/config': 9.22.0
'@wdio/config': 9.23.0
'@wdio/logger': 9.18.0
'@wdio/protocols': 9.16.2
'@wdio/types': 9.20.0
'@wdio/utils': 9.22.0
'@wdio/utils': 9.23.0
deepmerge-ts: 7.1.5
https-proxy-agent: 7.0.6
undici: 6.21.3
@ -30953,16 +30957,16 @@ snapshots:
- supports-color
- utf-8-validate
webdriverio@9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
webdriverio@9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
dependencies:
'@types/node': 20.19.25
'@types/sinonjs__fake-timers': 8.1.5
'@wdio/config': 9.22.0
'@wdio/config': 9.23.0
'@wdio/logger': 9.18.0
'@wdio/protocols': 9.16.2
'@wdio/repl': 9.16.2
'@wdio/types': 9.20.0
'@wdio/utils': 9.22.0
'@wdio/utils': 9.23.0
archiver: 7.0.1
aria-query: 5.3.2
cheerio: 1.1.2
@ -30979,7 +30983,7 @@ snapshots:
rgb2hex: 0.2.5
serialize-error: 12.0.0
urlpattern-polyfill: 10.1.0
webdriver: 9.22.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
webdriver: 9.23.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
transitivePeerDependencies:
- bare-buffer
- bufferutil

View File

@ -2,7 +2,7 @@ import { execSync } from "child_process";
import { build as esbuild } from "esbuild";
import { cpSync, existsSync, rmSync, writeFileSync } from "fs";
import { copySync, emptyDirSync, mkdirpSync } from "fs-extra";
import { join } from "path";
import { delimiter, join } from "path";
export default class BuildHelper {
@ -20,7 +20,7 @@ export default class BuildHelper {
copy(projectDirPath: string, outDirPath: string) {
let sourcePath: string;
if (projectDirPath.startsWith("/")) {
if (projectDirPath.startsWith("/") || projectDirPath.startsWith("\\")) {
sourcePath = join(this.rootDir, projectDirPath.substring(1));
} else {
sourcePath = join(this.projectDir, projectDirPath);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
import { LOCALES } from "../../packages/commons/src/lib/i18n";
import { getLanguageStats } from "./utils";
async function main() {
const languageStats = await getLanguageStats("client");
const localeIdsWithCoverage = languageStats.results
.filter(language => language.translated_percent > 50)
.map(language => language.language_code);
for (const localeId of localeIdsWithCoverage) {
const locale = LOCALES.find(l => l.id === localeId);
if (!locale) {
console.error(`Locale not found for id: ${localeId}`);
process.exit(1);
}
}
console.log("Translation coverage check passed.");
}
main();

View File

@ -1,42 +1,15 @@
import { readFile, stat, writeFile, } from "fs/promises";
import { readFile, writeFile } from "fs/promises";
import { join } from "path";
import { getLanguageStats } from "./utils";
const scriptDir = __dirname;
const rootDir = join(scriptDir, "../..");
const docsDir = join(rootDir, "docs");
async function getLanguageStats() {
const cacheFile = join(scriptDir, ".language-stats.json");
// Try to read from the cache.
try {
const cacheStats = await stat(cacheFile);
const now = new Date();
const oneDay = 24 * 60 * 60 * 1000; // milliseconds
if (cacheStats.mtimeMs < now.getTime() + oneDay) {
console.log("Reading language stats from cache.");
return JSON.parse(await readFile(cacheFile, "utf-8"));
}
} catch (e) {
if (!(e && typeof e === "object" && "code" in e && e.code === "ENOENT")) {
throw e;
}
}
// Make the request
console.log("Reading language stats from Weblate API.");
const request = await fetch("https://hosted.weblate.org/api/components/trilium/readme/translations/");
const stats = JSON.parse(await request.text());
// Update the cache
await writeFile(cacheFile, JSON.stringify(stats, null, 4));
return stats;
}
async function rewriteLanguageBar(readme: string) {
// Filter languages by their availability.
const languageStats = await getLanguageStats();
const languageStats = await getLanguageStats("readme");
const languagesWithCoverage: any[] = languageStats.results.filter(language => language.translated_percent > 75);
const languageLinks = languagesWithCoverage
.map(language => `[${language.language.name}](./${language.filename})`)

View File

@ -0,0 +1,33 @@
import { readFile, stat,writeFile } from "fs/promises";
import { join } from "path";
const scriptDir = __dirname;
export async function getLanguageStats(project: "readme" | "client") {
const cacheFile = join(scriptDir, `.language-stats-${project}.json`);
// Try to read from the cache.
try {
const cacheStats = await stat(cacheFile);
const now = new Date();
const oneDay = 24 * 60 * 60 * 1000; // milliseconds
if (cacheStats.mtimeMs < now.getTime() + oneDay) {
console.log("Reading language stats from cache.");
return JSON.parse(await readFile(cacheFile, "utf-8"));
}
} catch (e) {
if (!(e && typeof e === "object" && "code" in e && e.code === "ENOENT")) {
throw e;
}
}
// Make the request
console.log("Reading language stats from Weblate API.");
const request = await fetch(`https://hosted.weblate.org/api/components/trilium/${project}/translations/`);
const stats = JSON.parse(await request.text());
// Update the cache
await writeFile(cacheFile, JSON.stringify(stats, null, 4));
return stats;
}

View File

@ -5,6 +5,7 @@
"moduleResolution": "bundler",
"target": "es2023",
"outDir": "dist",
"rootDir": "..",
"types": [
"node",
"express"
@ -12,7 +13,8 @@
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
},
"include": [
"**/*.ts"
"scripts/*.ts",
"packages/commons/src/lib/i18n.ts"
],
"references": []
}

View File

@ -11,9 +11,9 @@
*
*/
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import fs from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
function processVersion(version) {
// Remove the beta suffix if any.
@ -42,14 +42,19 @@ function patchPackageJson(packageJsonPath) {
function main() {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const rootPackageJson = join(scriptDir, "..", "package.json");
patchPackageJson(rootPackageJson);
for (const app of ["server", "client"]) {
const appPackageJsonPath = join(scriptDir, "..", "apps", app, "package.json");
patchPackageJson(appPackageJsonPath);
}
for (const packageName of [ "pdfjs-viewer" ]) {
const packageJsonPath = join(scriptDir, "..", "packages", packageName, "package.json");
patchPackageJson(packageJsonPath);
}
}
main();