Merge branch 'main' into fix/sql_select_text
Some checks failed
Checks / main (push) Has been cancelled

This commit is contained in:
SngAbc 2026-01-05 13:51:45 +08:00 committed by GitHub
commit 458398f2ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 391 additions and 2091 deletions

1
.gitignore vendored
View File

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

View File

@ -194,7 +194,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
if (type === "pdf") { if (type === "pdf") {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>'); 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); $content.append($pdfPreview);
} else if (type === "audio") { } else if (type === "audio") {
@ -218,14 +218,14 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
// in attachment list // in attachment list
const $downloadButton = $(` const $downloadButton = $(`
<button class="file-download btn btn-primary" type="button"> <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")} ${t("file_properties.download")}
</button> </button>
`); `);
const $openButton = $(` const $openButton = $(`
<button class="file-open btn btn-primary" type="button"> <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")} ${t("file_properties.open")}
</button> </button>
`); `);

View File

@ -1596,7 +1596,9 @@
"toggle-sidebar": "切换侧边栏", "toggle-sidebar": "切换侧边栏",
"dropping-not-allowed": "不允许移动笔记到此处。", "dropping-not-allowed": "不允许移动笔记到此处。",
"shared-indicator-tooltip": "此笔记已公开分享", "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": { "title_bar_buttons": {
"window-on-top": "保持此窗口置顶" "window-on-top": "保持此窗口置顶"
@ -2194,7 +2196,14 @@
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。", "execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。",
"shared_copy_to_clipboard": "复制链接到剪贴板", "shared_copy_to_clipboard": "复制链接到剪贴板",
"shared_open_in_browser": "在浏览器中打开链接", "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": { "status_bar": {
"language_title": "更改内容语言", "language_title": "更改内容语言",

View File

@ -2196,7 +2196,14 @@
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。", "execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。",
"shared_copy_to_clipboard": "リンクをクリップボードにコピー", "shared_copy_to_clipboard": "リンクをクリップボードにコピー",
"shared_open_in_browser": "ブラウザでリンクを開く", "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": { "status_bar": {
"language_title": "コンテンツの言語を変更", "language_title": "コンテンツの言語を変更",

View File

@ -2200,7 +2200,14 @@
"read_only_temporarily_disabled_description": "此筆記目前可編輯,但通常為唯讀狀態。當您切換至其他筆記時,本筆記將立即恢復為唯讀模式。\n\n點擊此處重新啟用唯讀模式。", "read_only_temporarily_disabled_description": "此筆記目前可編輯,但通常為唯讀狀態。當您切換至其他筆記時,本筆記將立即恢復為唯讀模式。\n\n點擊此處重新啟用唯讀模式。",
"clipped_note_description": "本筆記原始來源為 {{url}}。\n\n點擊此處前往原網頁。", "clipped_note_description": "本筆記原始來源為 {{url}}。\n\n點擊此處前往原網頁。",
"execute_script_description": "此筆記為腳本筆記。點擊以執行腳本。", "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": { "breadcrumb": {
"hoisted_badge": "聚焦", "hoisted_badge": "聚焦",
@ -2246,6 +2253,6 @@
"pages_one": "共 {{count}} 頁", "pages_one": "共 {{count}} 頁",
"pages_other": "", "pages_other": "",
"pages_alt": "第 {{pageNumber}} 頁", "pages_alt": "第 {{pageNumber}} 頁",
"pages_loading": "載入…" "pages_loading": "正在載入…"
} }
} }

View File

@ -45,6 +45,10 @@ interface WithContext {
interface PdfDocumentModifiedMessage extends WithContext { interface PdfDocumentModifiedMessage extends WithContext {
type: "pdfjs-viewer-document-modified"; type: "pdfjs-viewer-document-modified";
}
interface PdfDocumentBlobResultMessage extends WithContext {
type: "pdfjs-viewer-blob";
data: Uint8Array<ArrayBufferLike>; data: Uint8Array<ArrayBufferLike>;
} }
@ -113,4 +117,5 @@ type PdfMessageEvent = MessageEvent<
| PdfViewerThumbnailMessage | PdfViewerThumbnailMessage
| PdfViewerAttachmentsMessage | PdfViewerAttachmentsMessage
| PdfViewerLayersMessage | 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 "./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 { Calendar as FullCalendar } from "@fullcalendar/core";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils"; import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
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 { DateClickArg } from "@fullcalendar/interaction"; import { DateClickArg } from "@fullcalendar/interaction";
import FNote from "../../../entities/fnote"; import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton";
import { RefObject } from "preact"; import { RefObject } from "preact";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar"; import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { openCalendarContextMenu } from "./context_menu";
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 { 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 { interface CalendarViewData {
@ -59,7 +61,7 @@ const CALENDAR_VIEWS = [
previousText: t("calendar.month_previous"), previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next") nextText: t("calendar.month_next")
} }
] ];
const SUPPORTED_CALENDAR_VIEW_TYPE = CALENDAR_VIEWS.map(v => v.type); 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"), ru: () => import("@fullcalendar/core/locales/ru"),
ja: () => import("@fullcalendar/core/locales/ja"), ja: () => import("@fullcalendar/core/locales/ja"),
pt: () => import("@fullcalendar/core/locales/pt"), pt: () => import("@fullcalendar/core/locales/pt"),
pl: () => import("@fullcalendar/core/locales/pl"),
"pt_br": () => import("@fullcalendar/core/locales/pt-br"), "pt_br": () => import("@fullcalendar/core/locales/pt-br"),
uk: () => import("@fullcalendar/core/locales/uk"), uk: () => import("@fullcalendar/core/locales/uk"),
en: null, en: null,
@ -102,9 +105,9 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const eventBuilder = useMemo(() => { const eventBuilder = useMemo(() => {
if (!isCalendarRoot) { if (!isCalendarRoot) {
return async () => await buildEvents(noteIds); return async () => await buildEvents(noteIds);
} else { }
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e); return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
}
}, [isCalendarRoot, noteIds]); }, [isCalendarRoot, noteIds]);
const plugins = usePlugins(isEditable, isCalendarRoot); 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()} /> <ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
</ButtonGroup> </ButtonGroup>
</div> </div>
) );
} }
function usePlugins(isEditable: boolean, isCalendarRoot: boolean) { function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
@ -293,7 +296,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
if (promotedAttributes) { if (promotedAttributes) {
let promotedAttributesHtml = ""; let promotedAttributesHtml = "";
for (const [name, value] of promotedAttributes) { for (const [name, value] of promotedAttributes) {
promotedAttributesHtml = promotedAttributesHtml + /*html*/`\ promotedAttributesHtml = `${promotedAttributesHtml /*html*/}\
<div class="promoted-attribute"> <div class="promoted-attribute">
<span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span> <span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span>
</div>`; </div>`;

View File

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

View File

@ -7,7 +7,6 @@ import attribute_renderer from "../../../services/attribute_renderer";
import content_renderer from "../../../services/content_renderer"; import content_renderer from "../../../services/content_renderer";
import { t } from "../../../services/i18n"; import { t } from "../../../services/i18n";
import link from "../../../services/link"; import link from "../../../services/link";
import tree from "../../../services/tree";
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks"; import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
import Icon from "../../react/Icon"; import Icon from "../../react/Icon";
import NoteLink from "../../react/NoteLink"; 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 }) { 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 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 ( return (
<div <div
@ -123,7 +113,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
> >
<h5 className="note-book-header"> <h5 className="note-book-header">
<Icon className="note-icon" icon={note.getIcon()} /> <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} /> <NoteAttributes note={note} />
</h5> </h5>
<NoteContent <NoteContent

View File

@ -170,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
return spacedUpdate; 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) { export function useNoteSavedData(noteId: string | undefined) {
return useSyncExternalStore( return useSyncExternalStore(
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {}, (cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},

View File

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

View File

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

View File

@ -4,9 +4,8 @@ import appContext from "../../../components/app_context";
import type NoteContext from "../../../components/note_context"; import type NoteContext from "../../../components/note_context";
import FBlob from "../../../entities/fblob"; import FBlob from "../../../entities/fblob";
import FNote from "../../../entities/fnote"; import FNote from "../../../entities/fnote";
import server from "../../../services/server";
import { useViewModeConfig } from "../../collections/NoteList"; import { useViewModeConfig } from "../../collections/NoteList";
import { useTriliumEvent } from "../../react/hooks"; import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import PdfViewer from "./PdfViewer"; import PdfViewer from "./PdfViewer";
export default function PdfPreview({ note, blob, componentId, noteContext }: { 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 iframeRef = useRef<HTMLIFrameElement>(null);
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory"); 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(() => { useEffect(() => {
function handleMessage(event: PdfMessageEvent) { function handleMessage(event: PdfMessageEvent) {
if (event.data?.type === "pdfjs-viewer-document-modified") { 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) { 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 ]); }, [ note, historyConfig, componentId, blob, noteContext ]);
// Refresh when blob changes.
useEffect(() => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
}, [ blob ]);
useTriliumEvent("customDownload", ({ ntxId }) => { useTriliumEvent("customDownload", ({ ntxId }) => {
if (ntxId !== noteContext.ntxId) return; if (ntxId !== noteContext.ntxId) return;
iframeRef.current?.contentWindow?.postMessage({ 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. */ /** Note: URLs are relative to /pdfjs/web. */
pdfUrl: string; pdfUrl: string;
onLoad?(): void; 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. * 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 iframeRef = useSyncedRef(externalIframeRef, null);
const [ locale ] = useTriliumOption("locale"); const [ locale ] = useTriliumOption("locale");
const [ newLayout ] = useTriliumOptionBool("newLayout"); const [ newLayout ] = useTriliumOptionBool("newLayout");
@ -30,7 +34,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad
<iframe <iframe
ref={iframeRef} ref={iframeRef}
class="pdf-preview" 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={() => { onLoad={() => {
injectStyles(); injectStyles();
onLoad?.(); onLoad?.();

View File

@ -70,6 +70,20 @@ test("Attachments listing works", async ({ page, context }) => {
await expect(attachmentsList.locator(".pdf-attachment-item")).toHaveCount(0); 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 }) => { test("Layers listing works", async ({ page, context }) => {
const app = new App(page, context); const app = new App(page, context);
await app.goto(); await app.goto();
@ -108,4 +122,8 @@ class PdfHelper {
async expectPageToBe(expectedPageNumber: number) { async expectPageToBe(expectedPageNumber: number) {
await expect(this.contentFrame.locator("#pageNumber")).toHaveValue(`${expectedPageNumber}`); 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 X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "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_cookie_path / /trilium/;
proxy_read_timeout 90; 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-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", "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", "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": { "dependencies": {
"better-sqlite3": "12.5.0", "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.mime = file.mimetype.toLowerCase();
note.save(); note.save();

View File

@ -3,7 +3,8 @@
"title": "Kom i gang", "title": "Kom i gang",
"architecture": "Arkitektur:", "architecture": "Arkitektur:",
"desktop_title": "Last ned skrivebordsprogram (v{{version}})", "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": { "hero_section": {
"title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.", "title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.",
@ -13,14 +14,19 @@
}, },
"organization_benefits": { "organization_benefits": {
"title": "Organisering", "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": { "productivity_benefits": {
"sync_title": "Synkronisering", "sync_title": "Synkronisering",
"search_title": "Kraftig søk", "search_title": "Kraftig søk",
"web_clipper_title": "Web clipper", "web_clipper_title": "Web clipper",
"revisions_title": "Notatrevisjon", "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": { "note_types": {
"canvas_title": "Kanvas", "canvas_title": "Kanvas",
@ -33,7 +39,8 @@
"extensibility_benefits": { "extensibility_benefits": {
"import_export_title": "Import/eksport", "import_export_title": "Import/eksport",
"scripting_title": "Avansert skripting", "scripting_title": "Avansert skripting",
"api_title": "REST API" "api_title": "REST API",
"title": "Deling og utvidbarhet"
}, },
"collections": { "collections": {
"title": "Samlinger", "title": "Samlinger",
@ -41,7 +48,8 @@
"table_title": "Tabell", "table_title": "Tabell",
"geomap_title": "Geokart", "geomap_title": "Geokart",
"presentation_title": "Presentasjon", "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": { "header": {
"documentation": "Dokumentasjon", "documentation": "Dokumentasjon",
@ -58,12 +66,15 @@
"paypal": "PayPal", "paypal": "PayPal",
"title": "Støtt oss", "title": "Støtt oss",
"financial_donations_title": "Finansiell donasjon", "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_helper_desktop_windows": {
"download_scoop": "Scoop", "download_scoop": "Scoop",
"title_x64": "Windows 64-bit", "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_helper_desktop_linux": {
"download_deb": ".deb", "download_deb": ".deb",
@ -72,15 +83,19 @@
"download_nixpkgs": "nixpkgs", "download_nixpkgs": "nixpkgs",
"download_aur": "AUR", "download_aur": "AUR",
"title_x64": "Linux 64-bit", "title_x64": "Linux 64-bit",
"download_zip": "Portable (.zip)" "download_zip": "Portable (.zip)",
"title_arm64": "Linux på ARM"
}, },
"download_helper_server_docker": { "download_helper_server_docker": {
"download_ghcr": "ghcr.io", "download_ghcr": "ghcr.io",
"download_dockerhub": "Docker Hub" "download_dockerhub": "Docker Hub",
"title": "Selv-hostet med Docker"
}, },
"download_helper_desktop_macos": { "download_helper_desktop_macos": {
"download_homebrew_cask": "Homebrew Cask", "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": { "final_cta": {
"get_started": "Kom i gang" "get_started": "Kom i gang"
@ -91,7 +106,9 @@
"download_now": { "download_now": {
"text": "Last ned nå ", "text": "Last ned nå ",
"platform_small": "for {{platform}}", "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": { "footer": {
"copyright_and_the": " og ", "copyright_and_the": " og ",
@ -100,9 +117,17 @@
"download_helper_server_linux": { "download_helper_server_linux": {
"download_tar_x64": "x64 (.tar.xz)", "download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.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": { "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", "value": "bx bxs-file-pdf",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
},
{
"type": "label",
"name": "shareAlias",
"value": "pdf",
"isInheritable": false,
"position": 60
} }
], ],
"format": "markdown", "format": "markdown",

View File

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

View File

@ -23,6 +23,7 @@ const UNSORTED_LOCALES = [
{ id: "ja", name: "日本語", electronLocale: "ja" }, { id: "ja", name: "日本語", electronLocale: "ja" },
{ id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR" }, { id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR" },
{ id: "pt", name: "Português (Portugal)", electronLocale: "pt_PT" }, { id: "pt", name: "Português (Portugal)", electronLocale: "pt_PT" },
{ id: "pl", name: "Polski", electronLocale: "pl" },
{ id: "ro", name: "Română", electronLocale: "ro" }, { id: "ro", name: "Română", electronLocale: "ro" },
{ id: "ru", name: "Русский", electronLocale: "ru" }, { id: "ru", name: "Русский", electronLocale: "ru" },
{ id: "tw", name: "繁體中文", electronLocale: "zh_TW" }, { 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 { build as esbuild } from "esbuild";
import { LOCALES } from "@triliumnext/commons"; import { LOCALES } from "@triliumnext/commons";
import { watch } from "chokidar"; 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 build = new BuildHelper("packages/pdfjs-viewer");
const watchMode = process.argv.includes("--watch"); const watchMode = process.argv.includes("--watch");
@ -16,6 +18,7 @@ async function main() {
for (const file of [ "viewer.css", "viewer.html", "viewer.mjs" ]) { for (const file of [ "viewer.css", "viewer.html", "viewer.mjs" ]) {
build.copy(`viewer/${file}`, `web/${file}`); build.copy(`viewer/${file}`, `web/${file}`);
} }
patchCacheBuster(`${build.outDir}/web/viewer.html`);
build.copy(`viewer/images`, `web/images`); build.copy(`viewer/images`, `web/images`);
// Copy the custom files. // Copy the custom files.
@ -34,8 +37,9 @@ async function main() {
build.writeJson("web/locale/locale.json", localeMappings); build.writeJson("web/locale/locale.json", localeMappings);
// Copy pdfjs-dist files. // Copy pdfjs-dist files.
build.copy("/node_modules/pdfjs-dist/build/pdf.mjs", "build/pdf.mjs"); for (const file of [ "pdf.mjs", "pdf.worker.mjs", "pdf.sandbox.mjs" ]) {
build.copy("/node_modules/pdfjs-dist/build/pdf.worker.mjs", "build/pdf.worker.mjs"); build.copy(join("/node_modules/pdfjs-dist/build", file), join("build", file));
}
if (watchMode) { if (watchMode) {
watchForChanges(); watchForChanges();
@ -59,6 +63,21 @@ async function rebuildCustomFiles() {
build.copy("src/custom.css", "web/custom.css"); 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() { function watchForChanges() {
console.log("Watching for changes in src directory..."); console.log("Watching for changes in src directory...");
const watcher = watch(join(build.projectDir, "src"), { const watcher = watch(join(build.projectDir, "src"), {

View File

@ -6,11 +6,14 @@ import { setupPdfLayers } from "./layers";
async function main() { async function main() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const isEditable = urlParams.get("editable") === "1";
if (urlParams.get("sidebar") === "0") { if (urlParams.get("sidebar") === "0") {
hideSidebar(); hideSidebar();
} }
interceptPersistence(getCustomAppOptions(urlParams)); if (isEditable) {
interceptPersistence(getCustomAppOptions(urlParams));
}
// Wait for the PDF viewer application to be available. // Wait for the PDF viewer application to be available.
while (!window.PDFViewerApplication) { while (!window.PDFViewerApplication) {
@ -18,16 +21,18 @@ async function main() {
} }
const app = window.PDFViewerApplication; const app = window.PDFViewerApplication;
app.eventBus.on("documentloaded", () => { if (isEditable) {
manageSave(); app.eventBus.on("documentloaded", () => {
manageDownload(); manageSave();
extractAndSendToc(); manageDownload();
setupScrollToHeading(); extractAndSendToc();
setupActiveHeadingTracking(); setupScrollToHeading();
setupPdfPages(); setupActiveHeadingTracking();
setupPdfAttachments(); setupPdfPages();
setupPdfLayers(); setupPdfAttachments();
}); setupPdfLayers();
});
}
await app.initializedPromise; await app.initializedPromise;
}; };
@ -55,37 +60,38 @@ function getCustomAppOptions(urlParams: URLSearchParams) {
function manageSave() { function manageSave() {
const app = window.PDFViewerApplication; const app = window.PDFViewerApplication;
const storage = app.pdfDocument.annotationStorage; const storage = app.pdfDocument.annotationStorage;
let timeout = null;
function debouncedSave() { function onChange() {
if (timeout) { if (!storage) return;
clearTimeout(timeout); window.parent.postMessage({
} type: "pdfjs-viewer-document-modified",
timeout = setTimeout(async () => { ntxId: window.TRILIUM_NTX_ID,
if (!storage) return; 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(); const data = await app.pdfDocument.saveDocument();
window.parent.postMessage({ window.parent.postMessage({
type: "pdfjs-viewer-document-modified", type: "pdfjs-viewer-blob",
data, data,
ntxId: window.TRILIUM_NTX_ID, ntxId: window.TRILIUM_NTX_ID,
noteId: window.TRILIUM_NOTE_ID noteId: window.TRILIUM_NOTE_ID
} satisfies PdfDocumentModifiedMessage, window.location.origin); } satisfies PdfDocumentBlobResultMessage, 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();
} }
}); });
app.pdfDocument.annotationStorage.onSetModified = () => {
onChange();
}; // works great for most cases, including forms.
app.eventBus.on("switchannotationeditorparams", () => {
onChange();
});
} }
function manageDownload() { function manageDownload() {

View File

@ -2,7 +2,7 @@ import { execSync } from "child_process";
import { build as esbuild } from "esbuild"; import { build as esbuild } from "esbuild";
import { cpSync, existsSync, rmSync, writeFileSync } from "fs"; import { cpSync, existsSync, rmSync, writeFileSync } from "fs";
import { copySync, emptyDirSync, mkdirpSync } from "fs-extra"; import { copySync, emptyDirSync, mkdirpSync } from "fs-extra";
import { join } from "path"; import { delimiter, join } from "path";
export default class BuildHelper { export default class BuildHelper {
@ -20,7 +20,7 @@ export default class BuildHelper {
copy(projectDirPath: string, outDirPath: string) { copy(projectDirPath: string, outDirPath: string) {
let sourcePath: string; let sourcePath: string;
if (projectDirPath.startsWith("/")) { if (projectDirPath.startsWith("/") || projectDirPath.startsWith("\\")) {
sourcePath = join(this.rootDir, projectDirPath.substring(1)); sourcePath = join(this.rootDir, projectDirPath.substring(1));
} else { } else {
sourcePath = join(this.projectDir, projectDirPath); 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 { join } from "path";
import { getLanguageStats } from "./utils";
const scriptDir = __dirname; const scriptDir = __dirname;
const rootDir = join(scriptDir, "../.."); const rootDir = join(scriptDir, "../..");
const docsDir = join(rootDir, "docs"); 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) { async function rewriteLanguageBar(readme: string) {
// Filter languages by their availability. // 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 languagesWithCoverage: any[] = languageStats.results.filter(language => language.translated_percent > 75);
const languageLinks = languagesWithCoverage const languageLinks = languagesWithCoverage
.map(language => `[${language.language.name}](./${language.filename})`) .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", "moduleResolution": "bundler",
"target": "es2023", "target": "es2023",
"outDir": "dist", "outDir": "dist",
"rootDir": "..",
"types": [ "types": [
"node", "node",
"express" "express"
@ -12,7 +13,8 @@
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo" "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
}, },
"include": [ "include": [
"**/*.ts" "scripts/*.ts",
"packages/commons/src/lib/i18n.ts"
], ],
"references": [] "references": []
} }

View File

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