mirror of
https://github.com/zadam/trilium.git
synced 2026-03-23 20:30:12 +01:00
Merge branch 'main' into totp
This commit is contained in:
2
.github/actions/build-server/action.yml
vendored
2
.github/actions/build-server/action.yml
vendored
@@ -8,7 +8,7 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
6
.github/workflows/dev.yml
vendored
6
.github/workflows/dev.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
- test_dev
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Update build info
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
||||
2
.github/workflows/i18n.yml
vendored
2
.github/workflows/i18n.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
4
.github/workflows/main-docker.yml
vendored
4
.github/workflows/main-docker.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
6
.github/workflows/nightly.yml
vendored
6
.github/workflows/nightly.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
|
||||
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
path: upload
|
||||
|
||||
- name: Publish stable release
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
with:
|
||||
draft: false
|
||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||
|
||||
4
.github/workflows/web-clipper.yml
vendored
4
.github/workflows/web-clipper.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
compression-level: 0
|
||||
|
||||
- name: Release web clipper extension
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/web-clipper-v') }}
|
||||
with:
|
||||
draft: false
|
||||
|
||||
2
.github/workflows/website.yml
vendored
2
.github/workflows/website.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.21.1",
|
||||
"@redocly/cli": "2.24.0",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.4",
|
||||
"js-yaml": "4.1.1",
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@univerjs/preset-sheets-conditional-formatting": "0.17.0",
|
||||
"@univerjs/preset-sheets-core": "0.17.0",
|
||||
"@univerjs/preset-sheets-data-validation": "0.17.0",
|
||||
"@univerjs/preset-sheets-filter": "0.17.0",
|
||||
"@univerjs/preset-sheets-find-replace": "0.17.0",
|
||||
"@univerjs/preset-sheets-note": "0.17.0",
|
||||
"@univerjs/preset-sheets-sort": "0.17.0",
|
||||
"@univerjs/presets": "0.17.0",
|
||||
"@zumer/snapdom": "2.1.0",
|
||||
"@univerjs/preset-sheets-conditional-formatting": "0.18.0",
|
||||
"@univerjs/preset-sheets-core": "0.18.0",
|
||||
"@univerjs/preset-sheets-data-validation": "0.18.0",
|
||||
"@univerjs/preset-sheets-filter": "0.18.0",
|
||||
"@univerjs/preset-sheets-find-replace": "0.18.0",
|
||||
"@univerjs/preset-sheets-note": "0.18.0",
|
||||
"@univerjs/preset-sheets-sort": "0.18.0",
|
||||
"@univerjs/presets": "0.18.0",
|
||||
"@zumer/snapdom": "2.5.0",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
@@ -59,7 +59,6 @@
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.38",
|
||||
"knockout": "3.5.2",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
@@ -92,4 +91,4 @@
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
|
||||
export type LabelType = "text" | "textarea" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
|
||||
type Multiplicity = "single" | "multi";
|
||||
|
||||
export interface DefinitionObject {
|
||||
@@ -17,7 +17,7 @@ function parse(value: string) {
|
||||
for (const token of tokens) {
|
||||
if (token === "promoted") {
|
||||
defObj.isPromoted = true;
|
||||
} else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
|
||||
} else if (["text", "textarea", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
|
||||
defObj.labelType = token as LabelType;
|
||||
} else if (["single", "multi"].includes(token)) {
|
||||
defObj.multiplicity = token as Multiplicity;
|
||||
|
||||
@@ -93,7 +93,7 @@ async function upload(url: string, fileToUpload: File, componentId?: string, met
|
||||
const formData = new FormData();
|
||||
formData.append("upload", fileToUpload);
|
||||
|
||||
return await $.ajax({
|
||||
const doUpload = async () => $.ajax({
|
||||
url: window.glob.baseApiUrl + url,
|
||||
headers: await getHeaders(componentId ? {
|
||||
"trilium-component-id": componentId
|
||||
@@ -104,6 +104,18 @@ async function upload(url: string, fileToUpload: File, componentId?: string, met
|
||||
contentType: false, // NEEDED, DON'T REMOVE THIS
|
||||
processData: false // NEEDED, DON'T REMOVE THIS
|
||||
});
|
||||
|
||||
try {
|
||||
return await doUpload();
|
||||
} catch (e: unknown) {
|
||||
// jQuery rejects with the jqXHR object
|
||||
const jqXhr = e as JQuery.jqXHR;
|
||||
if (jqXhr?.status && isCsrfError(jqXhr.status, jqXhr.responseText)) {
|
||||
await refreshCsrfToken();
|
||||
return await doUpload();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
let idCounter = 1;
|
||||
@@ -112,12 +124,55 @@ const idToRequestMap: Record<string, RequestData> = {};
|
||||
|
||||
let maxKnownEntityChangeId = 0;
|
||||
|
||||
let csrfRefreshInProgress: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Re-fetches /bootstrap to obtain a fresh CSRF token. This is needed when the
|
||||
* server session expires (e.g. mobile tab backgrounded for a long time) and the
|
||||
* existing CSRF token is no longer valid.
|
||||
*
|
||||
* Coalesces concurrent calls so only one bootstrap request is in-flight at a time.
|
||||
*/
|
||||
async function refreshCsrfToken(): Promise<void> {
|
||||
if (csrfRefreshInProgress) {
|
||||
return csrfRefreshInProgress;
|
||||
}
|
||||
|
||||
csrfRefreshInProgress = (async () => {
|
||||
try {
|
||||
const response = await fetch(`./bootstrap${window.location.search}`, { cache: "no-store" });
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
glob.csrfToken = json.csrfToken;
|
||||
}
|
||||
} finally {
|
||||
csrfRefreshInProgress = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return csrfRefreshInProgress;
|
||||
}
|
||||
|
||||
function isCsrfError(status: number, responseText: string): boolean {
|
||||
if (status !== 403) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const body = JSON.parse(responseText);
|
||||
return body.message === "Invalid CSRF token";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface CallOptions {
|
||||
data?: unknown;
|
||||
silentNotFound?: boolean;
|
||||
silentInternalServerError?: boolean;
|
||||
// If `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc.
|
||||
raw?: boolean;
|
||||
/** Used internally to prevent infinite retry loops on CSRF refresh. */
|
||||
csrfRetried?: boolean;
|
||||
}
|
||||
|
||||
async function call<T>(method: string, url: string, componentId?: string, options: CallOptions = {}) {
|
||||
@@ -167,7 +222,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, opts
|
||||
type: method,
|
||||
headers,
|
||||
timeout: 60000,
|
||||
success: (body, textStatus, jqXhr) => {
|
||||
success: (body, _textStatus, jqXhr) => {
|
||||
const respHeaders: Headers = {};
|
||||
|
||||
jqXhr
|
||||
@@ -192,7 +247,25 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, opts
|
||||
// don't report requests that are rejected by the browser, usually when the user is refreshing or going to a different page.
|
||||
rej("rejected by browser");
|
||||
return;
|
||||
} else if (opts.silentNotFound && jqXhr.status === 404) {
|
||||
}
|
||||
|
||||
// If the CSRF token is stale (e.g. session expired while tab was backgrounded),
|
||||
// refresh it and retry the request once.
|
||||
if (!opts.csrfRetried && isCsrfError(jqXhr.status, jqXhr.responseText)) {
|
||||
try {
|
||||
await refreshCsrfToken();
|
||||
// Rebuild headers so the fresh glob.csrfToken is picked up
|
||||
const retryHeaders = await getHeaders({ "trilium-component-id": headers["trilium-component-id"] });
|
||||
const retryResult = await ajax(url, method, data, retryHeaders, { ...opts, csrfRetried: true });
|
||||
res(retryResult);
|
||||
return;
|
||||
} catch (retryErr) {
|
||||
rej(retryErr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.silentNotFound && jqXhr.status === 404) {
|
||||
// report nothing
|
||||
} else if (opts.silentInternalServerError && jqXhr.status === 500) {
|
||||
// report nothing
|
||||
|
||||
@@ -1,67 +1,106 @@
|
||||
import "jquery";
|
||||
|
||||
import ko from "knockout";
|
||||
|
||||
import utils from "./services/utils.js";
|
||||
|
||||
// TriliumNextTODO: properly make use of below types
|
||||
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
|
||||
// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
|
||||
type SetupStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop" | "sync-from-server";
|
||||
type SetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
|
||||
|
||||
class SetupModel {
|
||||
syncInProgress: boolean;
|
||||
step: ko.Observable<string>;
|
||||
setupType: ko.Observable<string>;
|
||||
setupNewDocument: ko.Observable<boolean>;
|
||||
setupSyncFromDesktop: ko.Observable<boolean>;
|
||||
setupSyncFromServer: ko.Observable<boolean>;
|
||||
syncServerHost: ko.Observable<string | undefined>;
|
||||
syncProxy: ko.Observable<string | undefined>;
|
||||
password: ko.Observable<string | undefined>;
|
||||
totpToken: ko.Observable<string | undefined>;
|
||||
totpEnabled: ko.Observable<boolean>;
|
||||
class SetupController {
|
||||
private step: SetupStep;
|
||||
private setupType: SetupType = "";
|
||||
private syncPollIntervalId: number | null = null;
|
||||
private rootNode: HTMLElement;
|
||||
private setupTypeForm: HTMLFormElement;
|
||||
private syncFromServerForm: HTMLFormElement;
|
||||
private setupTypeNextButton: HTMLButtonElement;
|
||||
private setupTypeInputs: HTMLInputElement[];
|
||||
private syncServerHostInput: HTMLInputElement;
|
||||
private syncProxyInput: HTMLInputElement;
|
||||
private passwordInput: HTMLInputElement;
|
||||
private totpTokenInput: HTMLInputElement;
|
||||
private totpSection: HTMLElement;
|
||||
private totpEnabled = false;
|
||||
private sections: Record<SetupStep, HTMLElement>;
|
||||
|
||||
constructor(syncInProgress: boolean) {
|
||||
this.syncInProgress = syncInProgress;
|
||||
this.step = ko.observable(syncInProgress ? "sync-in-progress" : "setup-type");
|
||||
this.setupType = ko.observable("");
|
||||
this.setupNewDocument = ko.observable(false);
|
||||
this.setupSyncFromDesktop = ko.observable(false);
|
||||
this.setupSyncFromServer = ko.observable(false);
|
||||
this.syncServerHost = ko.observable();
|
||||
this.syncProxy = ko.observable();
|
||||
this.password = ko.observable();
|
||||
this.totpToken = ko.observable();
|
||||
this.totpEnabled = ko.observable(false);
|
||||
constructor(rootNode: HTMLElement, syncInProgress: boolean) {
|
||||
this.rootNode = rootNode;
|
||||
this.step = syncInProgress ? "sync-in-progress" : "setup-type";
|
||||
this.setupTypeForm = mustGetElement("setup-type-form", HTMLFormElement);
|
||||
this.syncFromServerForm = mustGetElement("sync-from-server-form", HTMLFormElement);
|
||||
this.setupTypeNextButton = mustGetElement("setup-type-next", HTMLButtonElement);
|
||||
this.setupTypeInputs = Array.from(document.querySelectorAll<HTMLInputElement>("input[name='setup-type']"));
|
||||
this.syncServerHostInput = mustGetElement("sync-server-host", HTMLInputElement);
|
||||
this.syncProxyInput = mustGetElement("sync-proxy", HTMLInputElement);
|
||||
this.passwordInput = mustGetElement("password", HTMLInputElement);
|
||||
this.totpTokenInput = mustGetElement("totp-token", HTMLInputElement);
|
||||
this.totpSection = mustGetElement("totp-section", HTMLElement);
|
||||
this.sections = {
|
||||
"setup-type": mustGetElement("setup-type-section", HTMLElement),
|
||||
"new-document-in-progress": mustGetElement("new-document-in-progress-section", HTMLElement),
|
||||
"sync-from-desktop": mustGetElement("sync-from-desktop-section", HTMLElement),
|
||||
"sync-from-server": mustGetElement("sync-from-server-section", HTMLElement),
|
||||
"sync-in-progress": mustGetElement("sync-in-progress-section", HTMLElement)
|
||||
};
|
||||
}
|
||||
|
||||
if (this.syncInProgress) {
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
init() {
|
||||
this.setupTypeForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void this.selectSetupType();
|
||||
});
|
||||
|
||||
this.syncFromServerForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void this.finish();
|
||||
});
|
||||
|
||||
for (const input of this.setupTypeInputs) {
|
||||
input.addEventListener("change", () => {
|
||||
this.setupType = input.value as SetupType;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
this.syncServerHostInput.addEventListener("blur", () => {
|
||||
void this.checkTotpStatus();
|
||||
});
|
||||
|
||||
for (const backButton of document.querySelectorAll<HTMLElement>("[data-action='back']")) {
|
||||
backButton.addEventListener("click", () => {
|
||||
this.back();
|
||||
});
|
||||
}
|
||||
|
||||
const serverAddress = `${location.protocol}//${location.host}`;
|
||||
$("#current-host").html(serverAddress);
|
||||
|
||||
if (this.step === "sync-in-progress") {
|
||||
this.startSyncPolling();
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.rootNode.style.display = "";
|
||||
}
|
||||
|
||||
// this is called in setup.ejs
|
||||
setupTypeSelected() {
|
||||
return !!this.setupType();
|
||||
}
|
||||
private async selectSetupType() {
|
||||
if (this.setupType === "new-document") {
|
||||
this.setStep("new-document-in-progress");
|
||||
|
||||
async selectSetupType() {
|
||||
if (this.setupType() === "new-document") {
|
||||
this.step("new-document-in-progress");
|
||||
await $.post("api/setup/new-document");
|
||||
window.location.replace("./setup");
|
||||
return;
|
||||
}
|
||||
|
||||
$.post("api/setup/new-document").then(() => {
|
||||
window.location.replace("./setup");
|
||||
});
|
||||
} else {
|
||||
this.step(this.setupType());
|
||||
if (this.setupType) {
|
||||
this.setStep(this.setupType);
|
||||
}
|
||||
}
|
||||
|
||||
async checkTotpStatus() {
|
||||
const syncServerHost = this.syncServerHost();
|
||||
private async checkTotpStatus() {
|
||||
const syncServerHost = this.syncServerHostInput.value.trim();
|
||||
|
||||
if (!syncServerHost) {
|
||||
this.totpEnabled(false);
|
||||
this.setTotpEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,22 +108,40 @@ class SetupModel {
|
||||
const resp = await $.post("api/setup/check-server-totp", {
|
||||
syncServerHost
|
||||
});
|
||||
this.totpEnabled(!!resp.totpEnabled);
|
||||
|
||||
this.setTotpEnabled(!!resp.totpEnabled);
|
||||
} catch {
|
||||
// If we can't reach the server, don't show TOTP field yet
|
||||
this.totpEnabled(false);
|
||||
// If we can't reach the server, don't show the TOTP field yet.
|
||||
this.setTotpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
back() {
|
||||
this.step("setup-type");
|
||||
this.setupType("");
|
||||
private setTotpEnabled(enabled: boolean) {
|
||||
this.totpEnabled = enabled;
|
||||
|
||||
if (!enabled) {
|
||||
this.totpTokenInput.value = "";
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
async finish() {
|
||||
const syncServerHost = this.syncServerHost();
|
||||
const syncProxy = this.syncProxy();
|
||||
const password = this.password();
|
||||
private back() {
|
||||
this.setStep("setup-type");
|
||||
this.setupType = "";
|
||||
this.setTotpEnabled(false);
|
||||
|
||||
for (const input of this.setupTypeInputs) {
|
||||
input.checked = false;
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
private async finish() {
|
||||
const syncServerHost = this.syncServerHostInput.value.trim();
|
||||
const syncProxy = this.syncProxyInput.value.trim();
|
||||
const password = this.passwordInput.value;
|
||||
|
||||
if (!syncServerHost) {
|
||||
showAlert("Trilium server address can't be empty");
|
||||
@@ -96,12 +153,11 @@ class SetupModel {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check TOTP status before submitting (in case it wasn't checked yet)
|
||||
await this.checkTotpStatus();
|
||||
|
||||
const totpToken = this.totpToken();
|
||||
const totpToken = this.totpTokenInput.value.trim();
|
||||
|
||||
if (this.totpEnabled() && !totpToken) {
|
||||
if (this.totpEnabled && !totpToken) {
|
||||
showAlert("TOTP token can't be empty when two-factor authentication is enabled");
|
||||
return;
|
||||
}
|
||||
@@ -115,15 +171,35 @@ class SetupModel {
|
||||
});
|
||||
|
||||
if (resp.result === "success") {
|
||||
this.step("sync-in-progress");
|
||||
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
|
||||
hideAlert();
|
||||
this.setStep("sync-in-progress");
|
||||
this.startSyncPolling();
|
||||
} else {
|
||||
showAlert(`Sync setup failed: ${resp.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private setStep(step: SetupStep) {
|
||||
this.step = step;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
for (const [step, section] of Object.entries(this.sections) as [SetupStep, HTMLElement][]) {
|
||||
section.style.display = step === this.step ? "" : "none";
|
||||
}
|
||||
|
||||
this.totpSection.style.display = this.totpEnabled ? "" : "none";
|
||||
this.setupTypeNextButton.disabled = !this.setupType;
|
||||
}
|
||||
|
||||
private startSyncPolling() {
|
||||
if (this.syncPollIntervalId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncPollIntervalId = window.setInterval(checkOutstandingSyncs, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkOutstandingSyncs() {
|
||||
@@ -157,9 +233,19 @@ function getSyncInProgress() {
|
||||
return !!parseInt(el.content);
|
||||
}
|
||||
|
||||
addEventListener("DOMContentLoaded", (event) => {
|
||||
function mustGetElement<T extends typeof HTMLElement>(id: string, ctor: T): InstanceType<T> {
|
||||
const element = document.getElementById(id);
|
||||
|
||||
if (!element || !(element instanceof ctor)) {
|
||||
throw new Error(`Expected element #${id}`);
|
||||
}
|
||||
|
||||
return element as InstanceType<T>;
|
||||
}
|
||||
|
||||
addEventListener("DOMContentLoaded", () => {
|
||||
const rootNode = document.getElementById("setup-dialog");
|
||||
if (!rootNode) return;
|
||||
ko.applyBindings(new SetupModel(getSyncInProgress()), rootNode);
|
||||
$("#setup-dialog").show();
|
||||
if (!rootNode || !(rootNode instanceof HTMLElement)) return;
|
||||
|
||||
new SetupController(rootNode, getSyncInProgress()).init();
|
||||
});
|
||||
|
||||
@@ -675,10 +675,11 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
div.alert {
|
||||
margin-bottom: 8px;
|
||||
background: var(--alert-bar-background) !important;
|
||||
color: var(--main-text-color);
|
||||
border-radius: 8px;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
div.alert p + p {
|
||||
margin-block: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,7 +1069,6 @@
|
||||
"rename_note": "اعادة تسمية الملاحظة",
|
||||
"remove_relation": "حذف العلاقة",
|
||||
"default_new_note_title": "ملاحظة جديدة",
|
||||
"open_in_new_tab": "فتح في تبويب جديد",
|
||||
"enter_new_title": "ادخل عنوان ملاحظة جديدة:",
|
||||
"note_not_found": "الملاحظة {{noteId}} غير موجودة!",
|
||||
"cannot_match_transform": "تعذر مطابقة التحويل: {{transform}}"
|
||||
|
||||
@@ -1047,7 +1047,6 @@
|
||||
"unprotecting-title": "解除保护状态"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "在新标签页中打开",
|
||||
"remove_note": "删除笔记",
|
||||
"edit_title": "编辑标题",
|
||||
"rename_note": "重命名笔记",
|
||||
|
||||
@@ -1046,7 +1046,6 @@
|
||||
"unprotecting-title": "Ungeschützt-Status"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "In neuem Tab öffnen",
|
||||
"remove_note": "Notiz entfernen",
|
||||
"edit_title": "Titel bearbeiten",
|
||||
"rename_note": "Notiz umbenennen",
|
||||
@@ -2183,5 +2182,52 @@
|
||||
},
|
||||
"setup_form": {
|
||||
"more_info": "Mehr erfahren"
|
||||
},
|
||||
"media": {
|
||||
"play": "Abspielen (Arbeitsbereich)",
|
||||
"pause": "Pausieren (Arbeitsbereich)",
|
||||
"back-10s": "10 s zurück (Linke Pfeiltaste)",
|
||||
"forward-30s": "30 s vorwärts",
|
||||
"mute": "Stumm (M)",
|
||||
"unmute": "Stummschaltung aufheben (M)",
|
||||
"playback-speed": "Wiedergabegeschwindigkeit",
|
||||
"loop": "Schleife",
|
||||
"disable-loop": "Schleife deaktivieren",
|
||||
"rotate": "Rotieren",
|
||||
"picture-in-picture": "Bild-in-Bild",
|
||||
"exit-picture-in-picture": "Bild-in-Bild verlassen",
|
||||
"fullscreen": "Vollbild (F)",
|
||||
"exit-fullscreen": "Vollbild verlassen",
|
||||
"unsupported-format": "Medienvorschau ist für dieses Format nicht verfügbar:\n{{mime}}",
|
||||
"zoom-to-fit": "Zoomen um auszufüllen",
|
||||
"zoom-reset": "Zoomen um auszufüllen zurücksetzen"
|
||||
},
|
||||
"mermaid": {
|
||||
"placeholder": "Geben den Inhalt des Mermaid-Diagramms ein oder verwenden eine der folgenden Beispieldiagramme.",
|
||||
"sample_diagrams": "Beispieldiagramme:",
|
||||
"sample_flowchart": "Flussdiagramm",
|
||||
"sample_class": "Klasse",
|
||||
"sample_sequence": "Abfolge",
|
||||
"sample_entity_relationship": "Entität Beziehung",
|
||||
"sample_state": "Zustandsübergangsdiagramm",
|
||||
"sample_mindmap": "Mindmap",
|
||||
"sample_architecture": "Architektur",
|
||||
"sample_block": "Block",
|
||||
"sample_c4": "C4",
|
||||
"sample_gantt": "Gantt",
|
||||
"sample_git": "GitGraph",
|
||||
"sample_kanban": "Kanban",
|
||||
"sample_packet": "Paket",
|
||||
"sample_pie": "Kuchen",
|
||||
"sample_quadrant": "Quadrant",
|
||||
"sample_radar": "Radar",
|
||||
"sample_requirement": "Anforderung",
|
||||
"sample_sankey": "Sankey",
|
||||
"sample_timeline": "Zeitstrahl",
|
||||
"sample_treemap": "Kachel",
|
||||
"sample_user_journey": "Benutzererfahrung",
|
||||
"sample_xy": "XY",
|
||||
"sample_venn": "Mengen",
|
||||
"sample_ishikawa": "Ursache-Wirkung"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +343,7 @@
|
||||
"label_type_title": "Type of the label will help Trilium to choose suitable interface to enter the label value.",
|
||||
"label_type": "Type",
|
||||
"text": "Text",
|
||||
"textarea": "Multi-line Text",
|
||||
"number": "Number",
|
||||
"boolean": "Boolean",
|
||||
"date": "Date",
|
||||
@@ -1068,7 +1069,6 @@
|
||||
"unprotecting-title": "Unprotecting status"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Open in new tab",
|
||||
"remove_note": "Remove note",
|
||||
"edit_title": "Edit title",
|
||||
"rename_note": "Rename note",
|
||||
|
||||
@@ -1051,7 +1051,6 @@
|
||||
"unprotecting-title": "Estado de desprotección"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Abrir en nueva pestaña",
|
||||
"remove_note": "Quitar nota",
|
||||
"edit_title": "Editar título",
|
||||
"rename_note": "Cambiar nombre de nota",
|
||||
@@ -2217,5 +2216,33 @@
|
||||
"unsupported-format": "La vista previa del medio no está disponible para este formato de archivo:\n{{mime}}",
|
||||
"zoom-to-fit": "Acercamiento para llenar",
|
||||
"zoom-reset": "Reiniciar acercamiento para llenar"
|
||||
},
|
||||
"mermaid": {
|
||||
"placeholder": "Ingrese el contenido de su diagrama Mermaid o utilice uno de los diagramas de muestra a continuación.",
|
||||
"sample_diagrams": "Diagramas de muestra:",
|
||||
"sample_flowchart": "Diagrama de flujo",
|
||||
"sample_class": "Clase",
|
||||
"sample_sequence": "Secuencia",
|
||||
"sample_entity_relationship": "Relación entre entidades",
|
||||
"sample_state": "Estado",
|
||||
"sample_mindmap": "Mapa mental",
|
||||
"sample_architecture": "Arquitectura",
|
||||
"sample_block": "Bloque",
|
||||
"sample_c4": "C4",
|
||||
"sample_gantt": "Gantt",
|
||||
"sample_git": "Git",
|
||||
"sample_kanban": "Kanban",
|
||||
"sample_packet": "Paquete",
|
||||
"sample_pie": "Pastel",
|
||||
"sample_quadrant": "Cuadrante",
|
||||
"sample_radar": "Radar",
|
||||
"sample_requirement": "Requerimiento",
|
||||
"sample_sankey": "Sankey",
|
||||
"sample_timeline": "Línea de tiempo",
|
||||
"sample_user_journey": "Jornada de usuario",
|
||||
"sample_xy": "XY",
|
||||
"sample_venn": "Venn",
|
||||
"sample_ishikawa": "Ishikawa",
|
||||
"sample_treemap": "Mapa de árbol"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1036,7 +1036,6 @@
|
||||
"unprotecting-title": "Statut de la non-protection"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
|
||||
"remove_note": "Supprimer la note",
|
||||
"edit_title": "Modifier le titre",
|
||||
"rename_note": "Renommer la note",
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"unprotecting-title": "Stádas díchosanta"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Oscail i gcluaisín nua",
|
||||
"remove_note": "Bain nóta",
|
||||
"edit_title": "Cuir an teideal in eagar",
|
||||
"rename_note": "Athainmnigh an nóta",
|
||||
@@ -2228,5 +2227,52 @@
|
||||
},
|
||||
"setup_form": {
|
||||
"more_info": "Foghlaim níos mó"
|
||||
},
|
||||
"media": {
|
||||
"play": "Seinn (Spás)",
|
||||
"pause": "Sos (Spás)",
|
||||
"back-10s": "10 soicind ar ais (eochair saighead chlé)",
|
||||
"forward-30s": "Ar aghaidh 30s",
|
||||
"mute": "Balbhaigh (M)",
|
||||
"unmute": "Díbhalbhaigh (M)",
|
||||
"playback-speed": "Luas athsheinm",
|
||||
"loop": "Lúb",
|
||||
"disable-loop": "Díchumasaigh an lúb",
|
||||
"rotate": "Rothlaigh",
|
||||
"picture-in-picture": "Pictiúr i bpictiúr",
|
||||
"exit-picture-in-picture": "Scoir pictiúr-i-bpictiúr",
|
||||
"fullscreen": "Lánscáileán (F)",
|
||||
"exit-fullscreen": "Scoir lánscáileáin",
|
||||
"unsupported-format": "Níl réamhamharc meán ar fáil don fhormáid comhaid seo:\n{{mime}}",
|
||||
"zoom-to-fit": "Zúmáil chun líonadh",
|
||||
"zoom-reset": "Athshocraigh súmáil chun líonadh"
|
||||
},
|
||||
"mermaid": {
|
||||
"placeholder": "Clóscríobh ábhar do léaráid Maighdean Mhara nó bain úsáid as ceann de na léaráidí samplacha thíos.",
|
||||
"sample_diagrams": "Léaráidí samplacha:",
|
||||
"sample_flowchart": "Cairt Sreabhadh",
|
||||
"sample_class": "Rang",
|
||||
"sample_sequence": "Seicheamh",
|
||||
"sample_entity_relationship": "Gaol Eintitis",
|
||||
"sample_state": "Stát",
|
||||
"sample_mindmap": "Léarscáil intinne",
|
||||
"sample_architecture": "Ailtireacht",
|
||||
"sample_block": "Bloc",
|
||||
"sample_c4": "C4",
|
||||
"sample_gantt": "Gantt",
|
||||
"sample_git": "Git",
|
||||
"sample_kanban": "Kanban",
|
||||
"sample_packet": "Paicéad",
|
||||
"sample_pie": "Pióg",
|
||||
"sample_quadrant": "Ceathrú",
|
||||
"sample_radar": "Radar",
|
||||
"sample_requirement": "Riachtanas",
|
||||
"sample_sankey": "Sankey",
|
||||
"sample_timeline": "Amlíne",
|
||||
"sample_treemap": "Léarscáil Crann",
|
||||
"sample_user_journey": "Turas Úsáideora",
|
||||
"sample_xy": "XY",
|
||||
"sample_venn": "Venn",
|
||||
"sample_ishikawa": "Ishikawa"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,7 +1049,6 @@
|
||||
"unprotecting-title": "अन-प्रोटेक्ट स्टेटस"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "नए टैब में खोलें",
|
||||
"remove_note": "नोट हटाएं",
|
||||
"edit_title": "टाइटल एडिट करें",
|
||||
"rename_note": "नोट का नाम बदलें",
|
||||
|
||||
@@ -1424,7 +1424,6 @@
|
||||
"unprotecting-title": "Stato non protetto"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Apri in una nuova scheda",
|
||||
"remove_note": "Rimuovi nota",
|
||||
"edit_title": "Modifica titolo",
|
||||
"rename_note": "Rinomina nota",
|
||||
|
||||
@@ -588,7 +588,7 @@
|
||||
"note-map": "ノートマップ",
|
||||
"render-note": "レンダリングノート",
|
||||
"book": "コレクション",
|
||||
"mermaid-diagram": "Mermaidダイアグラム",
|
||||
"mermaid-diagram": "マーメイド図",
|
||||
"canvas": "キャンバス",
|
||||
"web-view": "Web ビュー",
|
||||
"mind-map": "マインドマップ",
|
||||
@@ -1537,7 +1537,6 @@
|
||||
"url_placeholder": "http://web サイト..."
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "新しいタブで開く",
|
||||
"remove_note": "ノートを削除",
|
||||
"edit_title": "タイトルを編集",
|
||||
"rename_note": "ノート名を変更",
|
||||
@@ -2187,5 +2186,33 @@
|
||||
"unsupported-format": "このファイル形式ではメディアプレビューはご利用いただけません:\n{{mime}}",
|
||||
"zoom-to-fit": "ズームして全体を表示",
|
||||
"zoom-reset": "ズーム設定をリセット"
|
||||
},
|
||||
"mermaid": {
|
||||
"placeholder": "マーメイド図の内容を入力するか、以下のサンプル図のいずれかを使用してください。",
|
||||
"sample_diagrams": "サンプル図:",
|
||||
"sample_flowchart": "フローチャート",
|
||||
"sample_class": "クラス図",
|
||||
"sample_sequence": "シーケンス図",
|
||||
"sample_entity_relationship": "ER 図",
|
||||
"sample_state": "状態遷移図",
|
||||
"sample_mindmap": "マインドマップ",
|
||||
"sample_architecture": "アーキテクチャ図",
|
||||
"sample_block": "ブロック図",
|
||||
"sample_c4": "C4 図",
|
||||
"sample_gantt": "ガントチャート",
|
||||
"sample_git": "Git グラフ",
|
||||
"sample_kanban": "カンバン",
|
||||
"sample_packet": "パケット図",
|
||||
"sample_pie": "円グラフ",
|
||||
"sample_quadrant": "4象限図",
|
||||
"sample_radar": "レーダーチャート",
|
||||
"sample_requirement": "要件図",
|
||||
"sample_sankey": "サンキー図",
|
||||
"sample_timeline": "タイムライン",
|
||||
"sample_treemap": "ツリーマップ",
|
||||
"sample_user_journey": "ユーザージャーニー図",
|
||||
"sample_xy": "XY チャート",
|
||||
"sample_venn": "ベン図",
|
||||
"sample_ishikawa": "石川図"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"branch_prefix_saved": "브랜치 접두사가 저장되었습니다.",
|
||||
"edit_branch_prefix_multiple": "{{count}}개의 지점 접두사 편집",
|
||||
"branch_prefix_saved_multiple": "{{count}}개의 지점에 대해 지점 접두사가 저장되었습니다.",
|
||||
"affected_branches": "영향을 받는 브랜치 수 ({{count}}):"
|
||||
"affected_branches": "영향을 받은 분기 수({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "대량 작업",
|
||||
@@ -134,6 +134,27 @@
|
||||
"notSet": "미설정",
|
||||
"goBackForwards": "히스토리에서 뒤로/앞으로 이동",
|
||||
"showJumpToNoteDialog": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"노트로 이동\" 대화 상자</a> 표시",
|
||||
"scrollToActiveNote": "활성화된 노트로 스크롤 이동"
|
||||
"scrollToActiveNote": "활성화된 노트로 스크롤 이동",
|
||||
"collapseWholeTree": "모든 노트 트리를 접기",
|
||||
"collapseSubTree": "하위 트리 접기",
|
||||
"tabShortcuts": "탭 단축키",
|
||||
"onlyInDesktop": "데스크톱에서만(일렉트론 빌드)",
|
||||
"openEmptyTab": "빈 탭 열기",
|
||||
"closeActiveTab": "활성 탭 닫기",
|
||||
"jumpToParentNote": "부모 노트로 이동하기",
|
||||
"activateNextTab": "다음 탭 활성화",
|
||||
"activatePreviousTab": "이전 탭 활성화",
|
||||
"creatingNotes": "노트 만들기",
|
||||
"createNoteInto": "활성 노트에 새로운 하위 노트 추가",
|
||||
"movingCloningNotes": "노트 이동/복제",
|
||||
"moveNoteUpDown": "노트 목록에서 노트 위/아래 이동",
|
||||
"selectAllNotes": "현재 레벨의 모든 노트 선택",
|
||||
"selectNote": "노트 선택",
|
||||
"deleteNotes": "노트/하위트리 삭제",
|
||||
"editingNotes": "노트 편집",
|
||||
"createEditLink": "외부 링크 생성/편집",
|
||||
"createInternalLink": "내부 링크 생성",
|
||||
"followLink": "커서아래 링크 따라가기",
|
||||
"insertDateTime": "커서위치에 현재 날짜와 시간 삽입"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"widget-render-error": {
|
||||
"title": "Nie udało się wyrenderować niestandardowego widżetu React"
|
||||
},
|
||||
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.\nJeśli skrypt ma działać bez interfejsu użytkownika (UI) wyłącz go: '#run=frontendStartup'.",
|
||||
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.\n\nJeśli skrypt ma działać bez interfejsu użytkownika (UI) wyłącz go: '#run=frontendStartup'.",
|
||||
"open-script-note": "Otwórz notatkę ze skryptem",
|
||||
"scripting-error": "Błąd skryptu użytkownika: {{title}}"
|
||||
},
|
||||
@@ -1275,7 +1275,6 @@
|
||||
"unprotecting-title": "Status zdejmowania ochrony"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Otwórz w nowej karcie",
|
||||
"remove_note": "Usuń notatkę",
|
||||
"edit_title": "Edytuj tytuł",
|
||||
"rename_note": "Zmień nazwę notatki",
|
||||
@@ -1487,7 +1486,7 @@
|
||||
"custom_name_label": "Nazwa niestandardowej wyszukiwarki",
|
||||
"custom_name_placeholder": "Dostosuj nazwę wyszukiwarki",
|
||||
"custom_url_label": "URL niestandardowej wyszukiwarki powinien zawierać {keyword} jako symbol zastępczy dla wyszukiwanej frazy.",
|
||||
"custom_url_placeholder": "Dostosuj URL wyszukiwarki",
|
||||
"custom_url_placeholder": "Dostosuj url wyszukiwarki",
|
||||
"save_button": "Zapisz"
|
||||
},
|
||||
"tray": {
|
||||
@@ -2198,5 +2197,52 @@
|
||||
},
|
||||
"setup_form": {
|
||||
"more_info": "Dowiedz się więcej"
|
||||
},
|
||||
"media": {
|
||||
"fullscreen": "Pełny ekran (F)",
|
||||
"mute": "Wycisz (M)",
|
||||
"unmute": "Wyłącz wyciszenie (M)",
|
||||
"exit-fullscreen": "Wyłącz pełny ekran",
|
||||
"loop": "Pętla",
|
||||
"disable-loop": "Wyłącz pętle",
|
||||
"rotate": "Obróć",
|
||||
"picture-in-picture": "Obraz w obrazie",
|
||||
"pause": "Zatrzymaj (Space)",
|
||||
"back-10s": "Cofnij 10s (Lewa strzałka)",
|
||||
"forward-30s": "Do przodu 30s",
|
||||
"playback-speed": "Szybkość odtwarzania",
|
||||
"exit-picture-in-picture": "Wyjdź z obrazu w obrazie",
|
||||
"zoom-to-fit": "Powiększ aby wypełnić",
|
||||
"unsupported-format": "Podgląd multimediów nie jest dostępny dla tego formatu pliku\n{{mime}}",
|
||||
"play": "Odtwórz (Space)",
|
||||
"zoom-reset": "Zresetuj powiększenie"
|
||||
},
|
||||
"mermaid": {
|
||||
"sample_architecture": "Architektura",
|
||||
"sample_diagrams": "Przykład diagramu:",
|
||||
"sample_flowchart": "Schemat blokowy",
|
||||
"sample_class": "Klasa",
|
||||
"sample_sequence": "Sekwencja",
|
||||
"sample_timeline": "Oś czasu",
|
||||
"sample_treemap": "Mapa drzewa",
|
||||
"sample_xy": "XY",
|
||||
"sample_venn": "Diagram Venna",
|
||||
"sample_ishikawa": "Diagram Ishikawa",
|
||||
"placeholder": "Wpisz treść swojego diagramu lub skorzystaj z jednego z przykładowych diagramów poniżej.",
|
||||
"sample_entity_relationship": "Diagram związków encji",
|
||||
"sample_state": "Diagram stanów",
|
||||
"sample_mindmap": "Mapa myśli",
|
||||
"sample_block": "Diagram blokowy",
|
||||
"sample_c4": "C4",
|
||||
"sample_gantt": "Wykres Gantta",
|
||||
"sample_git": "Diagram Git",
|
||||
"sample_kanban": "Kanban",
|
||||
"sample_packet": "Diagram pakietów",
|
||||
"sample_pie": "Wykres kołowy",
|
||||
"sample_quadrant": "Diagram kwadrantowy",
|
||||
"sample_radar": "Wykres radarowy",
|
||||
"sample_requirement": "Diagram wymagań",
|
||||
"sample_sankey": "Wykres Sankeya",
|
||||
"sample_user_journey": "Mapa Podróży Użytkownika"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,7 +1047,6 @@
|
||||
"unprotecting-title": "Estado da remoção de proteção"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Abrir em nova guia",
|
||||
"remove_note": "Remover nota",
|
||||
"edit_title": "Editar título",
|
||||
"rename_note": "Renomear nota",
|
||||
|
||||
@@ -1111,7 +1111,6 @@
|
||||
"start_session_button": "Iniciar sessão protegida"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Abrir em nova aba",
|
||||
"remove_note": "Remover nota",
|
||||
"edit_title": "Editar título",
|
||||
"rename_note": "Renomear nota",
|
||||
|
||||
@@ -1054,7 +1054,6 @@
|
||||
"enter_title_of_new_note": "Introduceți titlul noii notițe",
|
||||
"note_already_in_diagram": "Notița „{{title}}” deja se află pe diagramă.",
|
||||
"note_not_found": "Notița „{{noteId}}” nu a putut fi găsită!",
|
||||
"open_in_new_tab": "Deschide într-un tab nou",
|
||||
"remove_note": "Șterge notița",
|
||||
"remove_relation": "Șterge relația",
|
||||
"rename_note": "Redenumește notița",
|
||||
|
||||
@@ -1625,7 +1625,6 @@
|
||||
"rename_note": "Переименовать заметку",
|
||||
"remove_relation": "Удалить отношение",
|
||||
"default_new_note_title": "новая заметка",
|
||||
"open_in_new_tab": "Открыть в новой вкладке",
|
||||
"confirm_remove_relation": "Вы уверены, что хотите удалить связь?",
|
||||
"enter_new_title": "Введите новое название заметки:",
|
||||
"note_not_found": "Заметка {{noteId}} не найдена!",
|
||||
|
||||
@@ -1046,7 +1046,6 @@
|
||||
"unprotecting-title": "解除保護狀態"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "在新分頁中打開",
|
||||
"remove_note": "刪除筆記",
|
||||
"edit_title": "編輯標題",
|
||||
"rename_note": "重新命名筆記",
|
||||
|
||||
@@ -1151,7 +1151,6 @@
|
||||
"unprotecting-title": "Статус зняття захисту"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Відкрити в новій вкладці",
|
||||
"remove_note": "Видалити нотатку",
|
||||
"edit_title": "Редагувати заголовок",
|
||||
"rename_note": "Перейменувати нотатку",
|
||||
|
||||
@@ -2,7 +2,7 @@ import "./PromotedAttributes.css";
|
||||
|
||||
import { UpdateAttributeResponse } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
|
||||
import { ComponentChild, createElement, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
|
||||
import { Dispatch, StateUpdater, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../components/note_context";
|
||||
@@ -36,7 +36,7 @@ interface CellProps {
|
||||
setCellToFocus(cell: Cell): void;
|
||||
}
|
||||
|
||||
type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
|
||||
type OnChangeEventData = TargetedEvent<HTMLInputElement | HTMLTextAreaElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
|
||||
type OnChangeListener = (e: OnChangeEventData) => void | Promise<void>;
|
||||
|
||||
export default function PromotedAttributes() {
|
||||
@@ -171,8 +171,9 @@ function PromotedAttributeCell(props: CellProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute> = {
|
||||
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute | undefined> = {
|
||||
text: "text",
|
||||
textarea: undefined,
|
||||
number: "number",
|
||||
boolean: "checkbox",
|
||||
date: "date",
|
||||
@@ -226,20 +227,21 @@ function LabelInput(props: CellProps & { inputId: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const inputNode = <input
|
||||
className="form-control promoted-attribute-input"
|
||||
tabIndex={200 + definitionAttr.position}
|
||||
id={inputId}
|
||||
type={LABEL_MAPPINGS[definition.labelType ?? "text"]}
|
||||
value={valueDraft}
|
||||
checked={definition.labelType === "boolean" ? valueAttr.value === "true" : undefined}
|
||||
placeholder={t("promoted_attributes.unset-field-placeholder")}
|
||||
data-attribute-id={valueAttr.attributeId}
|
||||
data-attribute-type={valueAttr.type}
|
||||
data-attribute-name={valueAttr.name}
|
||||
onBlur={onChangeListener}
|
||||
{...extraInputProps}
|
||||
/>;
|
||||
|
||||
const inputNode = createElement(definition.labelType === "textarea" ? "textarea" : "input", {
|
||||
className: "form-control promoted-attribute-input",
|
||||
tabIndex: 200 + definitionAttr.position,
|
||||
id: inputId,
|
||||
type: LABEL_MAPPINGS[definition.labelType ?? "text"],
|
||||
value: valueDraft,
|
||||
checked: definition.labelType === "boolean" ? valueAttr.value === "true" : undefined,
|
||||
placeholder: t("promoted_attributes.unset-field-placeholder"),
|
||||
"data-attribute-id": valueAttr.attributeId,
|
||||
"data-attribute-type": valueAttr.type,
|
||||
"data-attribute-name": valueAttr.name,
|
||||
onBlur: onChangeListener,
|
||||
...extraInputProps
|
||||
});
|
||||
|
||||
if (definition.labelType === "boolean") {
|
||||
return <>
|
||||
|
||||
@@ -137,6 +137,7 @@ const TPL = /*html*/`
|
||||
<td>
|
||||
<select class="attr-input-label-type form-control">
|
||||
<option value="text">${t("attribute_detail.text")}</option>
|
||||
<option value="textarea">${t("attribute_detail.textarea")}</option>
|
||||
<option value="number">${t("attribute_detail.number")}</option>
|
||||
<option value="boolean">${t("attribute_detail.boolean")}</option>
|
||||
<option value="date">${t("attribute_detail.date")}</option>
|
||||
|
||||
@@ -19,6 +19,13 @@ const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
|
||||
text: {
|
||||
editor: "input"
|
||||
},
|
||||
textarea: {
|
||||
editor: "textarea",
|
||||
formatter: "textarea",
|
||||
editorParams: {
|
||||
shiftEnterSubmit: true
|
||||
}
|
||||
},
|
||||
boolean: {
|
||||
formatter: "tickCross",
|
||||
editor: "tickCross"
|
||||
|
||||
@@ -75,3 +75,9 @@
|
||||
font-size: 1.5em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-editable {
|
||||
textarea {
|
||||
padding: 7px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Connection } from "jsplumb";
|
||||
import { RefObject } from "preact";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import contextMenu from "../../../menus/context_menu";
|
||||
import link_context_menu from "../../../menus/link_context_menu";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import RelationMapApi from "./api";
|
||||
import { Connection } from "jsplumb";
|
||||
|
||||
export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject<RelationMapApi>) {
|
||||
return (e: MouseEvent) => {
|
||||
@@ -17,22 +19,8 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{
|
||||
title: t("relation_map.open_in_new_tab"),
|
||||
uiIcon: "bx bx-empty",
|
||||
handler: () => appContext.tabManager.openTabWithNoteWithHoisting(note.noteId)
|
||||
},
|
||||
{
|
||||
title: t("relation_map.remove_note"),
|
||||
uiIcon: "bx bx-trash",
|
||||
handler: async () => {
|
||||
if (!note) return;
|
||||
const result = await dialog.confirmDeleteNoteBoxWithNote(note.title);
|
||||
if (typeof result !== "object" || !result.confirmed) return;
|
||||
|
||||
mapApiRef.current?.removeItem(note.noteId, result.isDeleteNoteChecked);
|
||||
}
|
||||
},
|
||||
...link_context_menu.getItems(e),
|
||||
{ kind: "separator" },
|
||||
{
|
||||
title: t("relation_map.edit_title"),
|
||||
uiIcon: "bx bx-pencil",
|
||||
@@ -49,10 +37,26 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA
|
||||
|
||||
await server.put(`notes/${note.noteId}/title`, { title });
|
||||
}
|
||||
}
|
||||
},
|
||||
{ kind: "separator" },
|
||||
|
||||
{
|
||||
title: t("relation_map.remove_note"),
|
||||
uiIcon: "bx bx-trash",
|
||||
handler: async () => {
|
||||
if (!note) return;
|
||||
const result = await dialog.confirmDeleteNoteBoxWithNote(note.title);
|
||||
if (typeof result !== "object" || !result.confirmed) return;
|
||||
|
||||
mapApiRef.current?.removeItem(note.noteId, result.isDeleteNoteChecked);
|
||||
}
|
||||
},
|
||||
],
|
||||
selectMenuItemHandler() {}
|
||||
})
|
||||
selectMenuItemHandler({ command }) {
|
||||
// Pass the events to the link context menu
|
||||
link_context_menu.handleLinkContextMenuItem(command, e, note.noteId);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"electron": "41.0.2",
|
||||
"electron": "41.0.3",
|
||||
"@electron-forge/cli": "7.11.1",
|
||||
"@electron-forge/maker-deb": "7.11.1",
|
||||
"@electron-forge/maker-dmg": "7.11.1",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"electron": "41.0.2",
|
||||
"electron": "41.0.3",
|
||||
"fs-extra": "11.3.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -75,8 +75,10 @@ test("Tabs are restored in right order", async ({ page, context }) => {
|
||||
await expect(app.getActiveTab()).toContainText("Mermaid");
|
||||
|
||||
// Select the mid one.
|
||||
const recentNotesSaved = page.waitForResponse((resp) => resp.url().includes("/api/recent-notes") && resp.ok());
|
||||
await (await app.getTab(1)).click();
|
||||
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
||||
await recentNotesSaved;
|
||||
|
||||
// Refresh the page and check the order.
|
||||
await app.goto( { preserveTabs: true });
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"main": "./src/main.ts",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"dev-alt": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_DATA_DIR=data2 TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"start-no-dir": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"edit-integration-db": "cross-env NODE_ENV=development TRILIUM_PORT=8086 TRILIUM_ENV=dev TRILIUM_DATA_DIR=spec/db TRILIUM_INTEGRATION_TEST=edit TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
@@ -78,11 +79,11 @@
|
||||
"cls-hooked": "4.2.2",
|
||||
"compression": "1.8.1",
|
||||
"cookie-parser": "1.4.7",
|
||||
"csrf-csrf": "4.0.2",
|
||||
"csrf-csrf": "4.0.3",
|
||||
"debounce": "3.0.0",
|
||||
"debug": "4.4.3",
|
||||
"ejs": "5.0.1",
|
||||
"electron": "41.0.2",
|
||||
"electron": "41.0.3",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
@@ -113,8 +114,8 @@
|
||||
"rand-token": "1.0.1",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.17.1",
|
||||
"sax": "1.5.0",
|
||||
"sanitize-html": "2.17.2",
|
||||
"sax": "1.6.0",
|
||||
"serve-favicon": "2.5.1",
|
||||
"stream-throttle": "0.1.3",
|
||||
"strip-bom": "5.0.0",
|
||||
|
||||
@@ -101,7 +101,9 @@
|
||||
"copy-without-formatting": "선택한 텍스트를 서식 없이 복사",
|
||||
"force-save-revision": "활성 노트의 새 버전을 강제로 생성/저장",
|
||||
"toggle-book-properties": "컬렉션 속성 토글",
|
||||
"toggle-classic-editor-toolbar": "고정 도구 모음 에디터의 서식 탭을 전환"
|
||||
"toggle-classic-editor-toolbar": "고정 도구 모음 에디터의 서식 탭을 전환",
|
||||
"export-as-pdf": "현재 노트를 PDF로 내보내기",
|
||||
"toggle-zen-mode": "젠 모드 활성화/비활성화(편집에 집중하기 위한 최소한의 UI)"
|
||||
},
|
||||
"hidden-subtree": {
|
||||
"zen-mode": "젠 모드",
|
||||
@@ -124,5 +126,90 @@
|
||||
"sync-title": "동기화",
|
||||
"other": "기타",
|
||||
"advanced-title": "고급"
|
||||
},
|
||||
"keyboard_action_names": {
|
||||
"back-in-note-history": "노트 기록으로 돌아가기",
|
||||
"forward-in-note-history": "노트 기록 앞으로 이동",
|
||||
"command-palette": "명령 팔레트",
|
||||
"scroll-to-active-note": "활성 노트로 스크롤",
|
||||
"quick-search": "빠른 검색",
|
||||
"search-in-subtree": "하위트리에서 검색",
|
||||
"expand-subtree": "하위트리 펼치기",
|
||||
"collapse-tree": "트리 접기",
|
||||
"collapse-subtree": "하위트리 접기",
|
||||
"sort-child-notes": "자식 노트 정렬",
|
||||
"create-note-into-inbox": "인박스에 노트 만들기",
|
||||
"delete-notes": "노트 삭제",
|
||||
"edit-note-title": "노트 제목 편집",
|
||||
"clone-notes-to": "다음으로 복사",
|
||||
"move-notes-to": "다음으로 노트 이동",
|
||||
"copy-notes-to-clipboard": "노트를 클립보드로 복사",
|
||||
"paste-notes-from-clipboard": "클립보드에서 노트 붙이기",
|
||||
"cut-notes-to-clipboard": "클립보드로 노트 잘라내기",
|
||||
"add-note-above-to-selection": "선택 위에 새로운 노트 추가",
|
||||
"add-note-below-to-selection": "선택 아래에 새로운 노트 추가",
|
||||
"duplicate-subtree": "하위트리 복제",
|
||||
"open-new-tab": "새로운탭 열기",
|
||||
"jump-to-note": "이동하기...",
|
||||
"move-note-down": "노트 아래로 이동",
|
||||
"move-note-up": "노트 위로 이동",
|
||||
"close-active-tab": "활성탭 닫기",
|
||||
"reopen-last-tab": "마지막 탭 다시 열기",
|
||||
"activate-next-tab": "다음 탭 활성화",
|
||||
"activate-previous-tab": "이전 탭 활성화",
|
||||
"open-new-window": "새창 열기",
|
||||
"toggle-system-tray-icon": "시스템 트레이 아이콘 토글",
|
||||
"toggle-zen-mode": "젠 모드 토글",
|
||||
"show-note-source": "노트 소스 보기",
|
||||
"show-options": "옵션 보기",
|
||||
"show-revisions": "리비전 보기",
|
||||
"show-recent-changes": "최근 변경사항 보기",
|
||||
"show-sql-console": "SQL 콘솔 보기",
|
||||
"show-backend-log": "백엔드 로그 보기",
|
||||
"show-help": "도움말 보기",
|
||||
"follow-link-under-cursor": "커서 아래 링크 따라 가기",
|
||||
"insert-date-and-time-to-text": "날짜와 시간 텍스트로 추가",
|
||||
"paste-markdown-into-text": "마크다운을 텍스트로 붙여넣기",
|
||||
"edit-read-only-note": "읽기 전용 노트 편집",
|
||||
"add-new-label": "새로운 라벨 추가",
|
||||
"add-new-relation": "새로운 관계 추가",
|
||||
"toggle-ribbon-tab-classic-editor": "클래식 에디터 리본 탭 토글",
|
||||
"toggle-ribbon-tab-basic-properties": "기본 설정 리본탭 토글",
|
||||
"print-active-note": "활성 노트 프린트",
|
||||
"export-active-note-as-pdf": "활성 노트를 PDF로 내보내기",
|
||||
"reload-frontend-app": "프론트엔드 앱 다시 로드",
|
||||
"open-developer-tools": "개발자 툴 열기",
|
||||
"find-in-text": "텍스트에서 찾기",
|
||||
"toggle-full-screen": "전체화면 토글",
|
||||
"zoom-out": "축소",
|
||||
"zoom-in": "확대",
|
||||
"reset-zoom-level": "확대/축소 다시 설정",
|
||||
"copy-without-formatting": "일반 텍스트로 복사",
|
||||
"force-save-revision": "리비전 강제 저장"
|
||||
},
|
||||
"login": {
|
||||
"title": "로그인",
|
||||
"heading": "Trilium 로그인",
|
||||
"incorrect-password": "암호가 맞지 않습니다. 다시 입력해 주세요.",
|
||||
"password": "암호",
|
||||
"button": "로그인",
|
||||
"sign_in_with_sso": "{{ ssoIssuerName }}로 로그인"
|
||||
},
|
||||
"set_password": {
|
||||
"title": "암호 설정",
|
||||
"heading": "암호 설정",
|
||||
"description": "Trilium을 웹에서 사용하기 전에 암호를 먼저 설정해야 합니다. 이 암호로 로그인하세요.",
|
||||
"password": "암호",
|
||||
"password-confirmation": "암호 확인",
|
||||
"button": "암호 설정"
|
||||
},
|
||||
"setup": {
|
||||
"heading": "Trilium 노트 셋업",
|
||||
"next": "다음",
|
||||
"init-in-progress": "문서 초기화 진행 중",
|
||||
"title": "셋업"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"step5": "연결 설정이 성공적인지 확인을 위해 \"Test sync\" 버튼을 클릭하세요."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,35 +58,35 @@
|
||||
<div class="alert alert-warning" id="alert" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
|
||||
<form data-bind="submit: selectSetupType">
|
||||
<div id="setup-type-section" style="margin-top: 20px;">
|
||||
<form id="setup-type-form">
|
||||
|
||||
<div class="radio" style="margin-bottom: 15px;">
|
||||
<label class="tn-radio">
|
||||
<input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
|
||||
<input type="radio" name="setup-type" value="new-document">
|
||||
<%= t("setup.new-document") %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio" style="margin-bottom: 15px;">
|
||||
<label class="tn-radio">
|
||||
<input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
|
||||
<input type="radio" name="setup-type" value="sync-from-desktop">
|
||||
<%= t("setup.sync-from-desktop") %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio" style="margin-bottom: 15px;">
|
||||
<label class="tn-radio">
|
||||
<input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
|
||||
<input type="radio" name="setup-type" value="sync-from-server">
|
||||
<%= t("setup.sync-from-server") %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-bind="disable: !setupTypeSelected()" class="btn btn-primary"><%= t("setup.next") %></button>
|
||||
<button type="submit" id="setup-type-next" class="btn btn-primary" disabled><%= t("setup.next") %></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'new-document-in-progress'">
|
||||
<div id="new-document-in-progress-section">
|
||||
<h2><%= t("setup.init-in-progress") %></h2>
|
||||
|
||||
<div style="display: flex; justify-content: flex-start; margin-top: 20px;">
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-from-desktop'">
|
||||
<div id="sync-from-desktop-section">
|
||||
<h2><%= t("setup_sync-from-desktop.heading") %></h2>
|
||||
|
||||
<p><%= t("setup_sync-from-desktop.description") %></p>
|
||||
@@ -117,11 +117,11 @@
|
||||
<li><%- t("setup_sync-from-desktop.step6", { link: `<a href="/">${t("setup_sync-from-desktop.step6-here")}</a>` }) %></li>
|
||||
</ol>
|
||||
|
||||
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
|
||||
<button type="button" data-action="back" class="btn btn-secondary">Back</button>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-from-server'">
|
||||
<form data-bind="submit: finish">
|
||||
<div id="sync-from-server-section">
|
||||
<form id="sync-from-server-form">
|
||||
|
||||
<h2><%= t("setup_sync-from-server.heading") %></h2>
|
||||
|
||||
@@ -129,31 +129,31 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-server-host"><%= t("setup_sync-from-server.server-host") %></label>
|
||||
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost, event: { blur: checkTotpStatus }" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
|
||||
<input type="text" id="sync-server-host" class="form-control" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sync-proxy"><%= t("setup_sync-from-server.proxy-server") %></label>
|
||||
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="<%= t("setup_sync-from-server.proxy-server-placeholder") %>">
|
||||
<input type="text" id="sync-proxy" class="form-control" placeholder="<%= t("setup_sync-from-server.proxy-server-placeholder") %>">
|
||||
|
||||
<p><strong><%= t("setup_sync-from-server.note") %></strong> <%= t("setup_sync-from-server.proxy-instruction") %></p>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 8px;">
|
||||
<label for="password"><%= t("setup_sync-from-server.password") %></label>
|
||||
<input type="password" id="password" class="form-control" data-bind="value: password" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
|
||||
<input type="password" id="password" class="form-control" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 8px;" data-bind="visible: totpEnabled">
|
||||
<label for="totpToken"><%= t("setup_sync-from-server.totp-token") %></label>
|
||||
<input type="text" id="totpToken" class="form-control" data-bind="value: totpToken" placeholder="<%= t("setup_sync-from-server.totp-token-placeholder") %>" autocomplete="one-time-code">
|
||||
<div id="totp-section" class="form-group" style="margin-bottom: 8px; display: none;">
|
||||
<label for="totp-token"><%= t("setup_sync-from-server.totp-token") %></label>
|
||||
<input type="text" id="totp-token" class="form-control" placeholder="<%= t("setup_sync-from-server.totp-token-placeholder") %>" autocomplete="one-time-code">
|
||||
</div>
|
||||
|
||||
<button type="button" data-bind="click: back" class="btn btn-secondary"><%= t("setup_sync-from-server.back") %></button>
|
||||
<button type="button" data-action="back" class="btn btn-secondary"><%= t("setup_sync-from-server.back") %></button>
|
||||
|
||||
<button type="submit" class="btn btn-primary"><%= t("setup_sync-from-server.finish-setup") %></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-in-progress'">
|
||||
<div id="sync-in-progress-section">
|
||||
<h2><%= t("setup_sync-in-progress.heading") %></h2>
|
||||
|
||||
<div class="alert alert-success"><%= t("setup_sync-in-progress.successful") %></div>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import crypto from "crypto";
|
||||
import { doubleCsrf } from "csrf-csrf";
|
||||
|
||||
import sessionSecret from "../services/session_secret.js";
|
||||
import { isElectron } from "../services/utils.js";
|
||||
|
||||
export const CSRF_COOKIE_NAME = "trilium-csrf";
|
||||
|
||||
// In Electron, API calls go through an IPC bypass (routes/electron.ts) that uses a
|
||||
// FakeRequest with a static session ID, while the bootstrap request goes through real
|
||||
// Express with a real session. This mismatch causes CSRF validation to always fail.
|
||||
// We use a per-instance random identifier so each Electron process still gets unique tokens.
|
||||
const electronSessionId = crypto.randomUUID();
|
||||
|
||||
const doubleCsrfUtilities = doubleCsrf({
|
||||
getSecret: () => sessionSecret,
|
||||
cookieOptions: {
|
||||
@@ -13,7 +21,7 @@ const doubleCsrfUtilities = doubleCsrf({
|
||||
httpOnly: !isElectron // set to false for Electron, see https://github.com/TriliumNext/Trilium/pull/966
|
||||
},
|
||||
cookieName: CSRF_COOKIE_NAME,
|
||||
getSessionIdentifier: (req) => req.session.id
|
||||
getSessionIdentifier: (req) => isElectron ? electronSessionId : req.session.id
|
||||
});
|
||||
|
||||
export const { generateCsrfToken, doubleCsrfProtection } = doubleCsrfUtilities;
|
||||
|
||||
@@ -7,7 +7,7 @@ function parse(value: string): DefinitionObject {
|
||||
for (const token of tokens) {
|
||||
if (token === "promoted") {
|
||||
defObj.isPromoted = true;
|
||||
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
|
||||
} else if (["text", "textarea", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
|
||||
defObj.labelType = token;
|
||||
} else if (["single", "multi"].includes(token)) {
|
||||
defObj.multiplicity = token;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import safeCompare from "safe-compare";
|
||||
|
||||
import type { Request, Response, Router } from "express";
|
||||
import type { NextFunction, Request, Response, Router } from "express";
|
||||
|
||||
import shaca from "./shaca/shaca.js";
|
||||
import shacaLoader from "./shaca/shaca_loader.js";
|
||||
@@ -10,6 +10,16 @@ import type SNote from "./shaca/entities/snote.js";
|
||||
import type SAttachment from "./shaca/entities/sattachment.js";
|
||||
import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { isShareDbReady } from "./sql.js";
|
||||
|
||||
function assertShareDbReady(_req: Request, res: Response, next: NextFunction) {
|
||||
if (!isShareDbReady()) {
|
||||
res.status(503).send("The application is still initializing. Please try again in a moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function addNoIndexHeader(note: SNote, res: Response) {
|
||||
if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
|
||||
@@ -115,6 +125,8 @@ function render404(res: Response) {
|
||||
}
|
||||
|
||||
function register(router: Router) {
|
||||
// Guard: if the share DB is not yet initialized, return 503 for all /share routes.
|
||||
router.use("/share", assertShareDbReady);
|
||||
|
||||
function renderNote(note: SNote, req: Request, res: Response) {
|
||||
if (!note) {
|
||||
|
||||
@@ -5,12 +5,14 @@ import dataDir from "../services/data_dir.js";
|
||||
import sql_init from "../services/sql_init.js";
|
||||
|
||||
let dbConnection!: Database.Database;
|
||||
let dbConnectionReady = false;
|
||||
|
||||
sql_init.dbReady.then(() => {
|
||||
dbConnection = new Database(dataDir.DOCUMENT_PATH, {
|
||||
readonly: true,
|
||||
nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined
|
||||
});
|
||||
dbConnectionReady = true;
|
||||
|
||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
|
||||
process.on(eventType, () => {
|
||||
@@ -23,18 +25,31 @@ sql_init.dbReady.then(() => {
|
||||
});
|
||||
});
|
||||
|
||||
function assertDbReady(): void {
|
||||
if (!dbConnectionReady) {
|
||||
throw new Error("Share database connection is not yet ready. The application may still be initializing.");
|
||||
}
|
||||
}
|
||||
|
||||
function getRawRows<T>(query: string, params = []): T[] {
|
||||
assertDbReady();
|
||||
return dbConnection.prepare(query).raw().all(params) as T[];
|
||||
}
|
||||
|
||||
function getRow<T>(query: string, params: string[] = []): T {
|
||||
assertDbReady();
|
||||
return dbConnection.prepare(query).get(params) as T;
|
||||
}
|
||||
|
||||
function getColumn<T>(query: string, params: string[] = []): T[] {
|
||||
assertDbReady();
|
||||
return dbConnection.prepare(query).pluck().all(params) as T[];
|
||||
}
|
||||
|
||||
export function isShareDbReady(): boolean {
|
||||
return dbConnectionReady;
|
||||
}
|
||||
|
||||
export default {
|
||||
getRawRows,
|
||||
getRow,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"devDependencies": {
|
||||
"@wxt-dev/auto-icons": "1.1.1",
|
||||
"wxt": "0.20.18"
|
||||
"wxt": "0.20.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"cash-dom": "8.1.5"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"react-i18next": "16.5.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "2.10.3",
|
||||
"@preact/preset-vite": "2.10.4",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-preact": "2.0.0",
|
||||
"typescript": "5.9.3",
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
"file_description": "PDF、画像、ビデオなどのマルチメディア ファイルをアプリケーション内にプレビュー表示で埋め込みます。",
|
||||
"canvas_title": "キャンバス",
|
||||
"canvas_description": "excalidraw.comと同じ技術を使って、無限のキャンバスに図形、画像、テキストを配置できます。図表、スケッチ、ビジュアルプランニングに最適です。",
|
||||
"mermaid_title": "Mermaid ダイアグラム",
|
||||
"mermaid_description": "Mermaid 構文を使用して、フローチャート、クラス図、シーケンス図、ガント チャートなどの図を作成します。",
|
||||
"mermaid_title": "マーメイド図",
|
||||
"mermaid_description": "マーメイド構文を使用して、フローチャート、クラス図、シーケンス図、ガント チャートなどの図を作成します。",
|
||||
"mindmap_title": "マインドマップ",
|
||||
"mindmap_description": "考えを視覚的に整理したり、ブレインストーミング セッションを行ったりします。",
|
||||
"others_list": "その他: <0>ノートマップ</0>、<1>リレーションマップ</1>、<2>保存された検索</2>、<3>レンダリングノート</3>、<4>web ビュー</4>。",
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
"database_question": "어디에 데이터가 저장되나요?",
|
||||
"server_question": "Trilium을 사용하기 위해 서버가 필요한가요?",
|
||||
"title": "자주 묻는 질문",
|
||||
"database_answer": "모든 노트는 애플리케이션 폴더의 SQLite 데이터베이스에 저장됩니다. Trilium이 텍스트 파일 대신 데이터베이스를 사용하는 이유는 성능과 기능 모두 구현하기 훨씬 어렵기 때문입니다(트리 여러 위치에 같은 노트를 두는 Clone과 같은 기능). 애플리케이션 폴더를 찾으려면 About 창으로 가세요.",
|
||||
"database_answer": "모든 노트는 앱 폴더의 SQLite 데이터베이스에 저장됩니다. Trilium이 텍스트파일 대신 데이터베이스를 사용하는 이유는 성능 향상과 복제(트리의 여러 위치에 같은 노트) 같은 훨씬 더 구현하기 어려운 몇몇 기능들 때문입니다. Trilium Notes에 대해서 창에서 앱 폴더를 찾을 수 있습니다.",
|
||||
"server_answer": "아니요, 서버는 웹 브라우저를 통해 접속할 수 있도록 허용하며, 여러 기기를 사용하는 경우 동기화를 관리합니다. 시작하려면 데스크톱 애플리케이션을 다운로드하여 사용하기만 하면 됩니다.",
|
||||
"scaling_question": "이 애플리케이션은 얼마나 많은 노트를 처리할 수 있나요?",
|
||||
"scaling_answer": "사용량에 따라 다르겠지만, 이 애플리케이션은 최소 10만 개의 노트를 문제없이 처리할 수 있습니다. 다만, Trilium은 (NextCloud와 같은) 파일 저장소라기보다는 지식 기반 애플리케이션에 가깝기 때문에, 대용량 파일(파일당 1GB 이상)을 많이 업로드할 경우 동기화 과정이 실패할 수 있다는 점에 유의하십시오.",
|
||||
|
||||
2
docs/README-ko.md
vendored
2
docs/README-ko.md
vendored
@@ -289,7 +289,7 @@ Consider supporting the main developer
|
||||
- [PayPal](https://paypal.me/eliandoran)
|
||||
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
|
||||
|
||||
## 🔑 License
|
||||
## 🔑라이센스
|
||||
|
||||
Copyright 2017-2025 zadam, Elian Doran, and other contributors
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"server:build": "pnpm run --filter server build",
|
||||
"server:coverage": "pnpm run --filter server test --coverage",
|
||||
"server:start": "pnpm run --filter server dev",
|
||||
"server:start-alt": "pnpm run --filter server dev-alt",
|
||||
"server:start-prod": "pnpm run --filter server start-prod",
|
||||
"desktop:start": "pnpm run --filter desktop dev",
|
||||
"desktop:build": "pnpm run --filter desktop build",
|
||||
@@ -61,7 +62,7 @@
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-preact": "2.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-playwright": "2.9.0",
|
||||
"eslint-plugin-playwright": "2.10.1",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"happy-dom": "20.8.4",
|
||||
"http-server": "14.1.1",
|
||||
@@ -73,7 +74,7 @@
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.0",
|
||||
"typescript-eslint": "8.57.1",
|
||||
"upath": "2.0.1",
|
||||
"vite": "8.0.0",
|
||||
"vite-plugin-dts": "4.5.4",
|
||||
@@ -118,7 +119,7 @@
|
||||
"on-headers@<1.1.0": ">=1.1.0",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"form-data@>=3.0.0 <3.0.4": ">=3.0.4",
|
||||
"node-abi": "4.27.0",
|
||||
"node-abi": "4.28.0",
|
||||
"validator@<13.15.20": ">=13.15.20",
|
||||
"tmp@<=0.2.3": ">=0.2.4",
|
||||
"glob@>=10.2.0 <10.5.0": ">=10.5.0",
|
||||
|
||||
@@ -24,22 +24,22 @@
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"@typescript-eslint/parser": "8.57.1",
|
||||
"@vitest/browser": "4.1.0",
|
||||
"@vitest/coverage-istanbul": "4.1.0",
|
||||
"ckeditor5": "47.6.1",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.3.3",
|
||||
"lint-staged": "16.4.0",
|
||||
"stylelint": "17.4.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.1.0",
|
||||
"webdriverio": "9.25.0"
|
||||
"webdriverio": "9.26.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.6.1"
|
||||
|
||||
@@ -25,22 +25,22 @@
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"@typescript-eslint/parser": "8.57.1",
|
||||
"@vitest/browser": "4.1.0",
|
||||
"@vitest/coverage-istanbul": "4.1.0",
|
||||
"ckeditor5": "47.6.1",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.3.3",
|
||||
"lint-staged": "16.4.0",
|
||||
"stylelint": "17.4.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.1.0",
|
||||
"webdriverio": "9.25.0"
|
||||
"webdriverio": "9.26.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.6.1"
|
||||
|
||||
@@ -27,22 +27,22 @@
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"@typescript-eslint/parser": "8.57.1",
|
||||
"@vitest/browser": "4.1.0",
|
||||
"@vitest/coverage-istanbul": "4.1.0",
|
||||
"ckeditor5": "47.6.1",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.3.3",
|
||||
"lint-staged": "16.4.0",
|
||||
"stylelint": "17.4.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.1.0",
|
||||
"webdriverio": "9.25.0"
|
||||
"webdriverio": "9.26.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.6.1"
|
||||
|
||||
@@ -27,22 +27,22 @@
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"@typescript-eslint/parser": "8.57.1",
|
||||
"@vitest/browser": "4.1.0",
|
||||
"@vitest/coverage-istanbul": "4.1.0",
|
||||
"ckeditor5": "47.6.1",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.3.3",
|
||||
"lint-staged": "16.4.0",
|
||||
"stylelint": "17.4.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.1.0",
|
||||
"webdriverio": "9.25.0"
|
||||
"webdriverio": "9.26.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.6.1"
|
||||
|
||||
@@ -27,22 +27,22 @@
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"@typescript-eslint/parser": "8.57.1",
|
||||
"@vitest/browser": "4.1.0",
|
||||
"@vitest/coverage-istanbul": "4.1.0",
|
||||
"ckeditor5": "47.6.1",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.3.3",
|
||||
"lint-staged": "16.4.0",
|
||||
"stylelint": "17.4.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.1.0",
|
||||
"webdriverio": "9.25.0"
|
||||
"webdriverio": "9.26.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.6.1"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"ckeditor5-premium-features": "47.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@smithy/middleware-retry": "4.4.42",
|
||||
"@smithy/middleware-retry": "4.4.43",
|
||||
"@types/jquery": "4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@catppuccin/highlightjs": "1.0.1",
|
||||
"@exercism/highlightjs-gdscript": "0.0.1",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"highlight.js": "11.11.1",
|
||||
|
||||
@@ -47,6 +47,22 @@ export function highlight(code: string, options: HighlightOptions) {
|
||||
return hljs.highlight(code, options);
|
||||
}
|
||||
|
||||
export function normalizeThemeCss(themeCss: string): string {
|
||||
const themeSelectorScopedToCodeTag = /\bcode\s+\.hljs-/.test(themeCss);
|
||||
if (themeSelectorScopedToCodeTag) {
|
||||
themeCss = themeCss.replace(/\bcode\.hljs/g, ".hljs");
|
||||
themeCss = themeCss.replace(/\bcode\s+\.hljs-/g, ".hljs .hljs-");
|
||||
}
|
||||
|
||||
// Increase the specificity of the HLJS selector to render properly within CKEditor without the need of patching the library.
|
||||
themeCss = themeCss.replace(
|
||||
/^\.hljs\s*\{/m,
|
||||
".hljs, .ck-content pre.hljs {",
|
||||
);
|
||||
|
||||
return themeCss;
|
||||
}
|
||||
|
||||
export async function loadTheme(theme: "none" | Theme) {
|
||||
if (theme === "none") {
|
||||
if (highlightingThemeEl) {
|
||||
@@ -61,12 +77,8 @@ export async function loadTheme(theme: "none" | Theme) {
|
||||
document.querySelector("head")?.append(highlightingThemeEl);
|
||||
}
|
||||
|
||||
let themeCss = (await theme.load()).default as string;
|
||||
|
||||
// Increase the specificity of the HLJS selector to render properly within CKEditor without the need of patching the library.
|
||||
themeCss = themeCss.replace(/^.hljs {/m, ".hljs, .ck-content pre.hljs {");
|
||||
|
||||
highlightingThemeEl.textContent = themeCss;
|
||||
const themeCss = (await theme.load()).default as string;
|
||||
highlightingThemeEl.textContent = normalizeThemeCss(themeCss);
|
||||
}
|
||||
|
||||
export const { highlightAuto } = hljs;
|
||||
|
||||
82
packages/highlightjs/src/normalize_theme_css.spec.ts
Normal file
82
packages/highlightjs/src/normalize_theme_css.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeThemeCss } from "./index.js";
|
||||
|
||||
describe("normalizeThemeCss", () => {
|
||||
describe("standard highlight.js themes", () => {
|
||||
const standardThemeCss = [
|
||||
"pre code.hljs {",
|
||||
" display: block;",
|
||||
" overflow-x: auto;",
|
||||
" padding: 1em",
|
||||
"}",
|
||||
"code.hljs {",
|
||||
" padding: 3px 5px",
|
||||
"}",
|
||||
".hljs {",
|
||||
" color: #ffffff;",
|
||||
" background: #1c1b1b",
|
||||
"}",
|
||||
".hljs-keyword {",
|
||||
" color: #88aece",
|
||||
"}",
|
||||
".hljs-string {",
|
||||
" color: #b5bd68",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
it("preserves 'pre code.hljs' layout rule", () => {
|
||||
const result = normalizeThemeCss(standardThemeCss);
|
||||
expect(result).toContain("pre code.hljs {");
|
||||
});
|
||||
|
||||
it("preserves 'code.hljs' inline layout rule", () => {
|
||||
const result = normalizeThemeCss(standardThemeCss);
|
||||
expect(result).toContain("code.hljs {");
|
||||
});
|
||||
|
||||
it("preserves .hljs-* token selectors unchanged", () => {
|
||||
const result = normalizeThemeCss(standardThemeCss);
|
||||
expect(result).toContain(".hljs-keyword {");
|
||||
expect(result).toContain(".hljs-string {");
|
||||
});
|
||||
|
||||
it("adds CKEditor specificity to .hljs container rule", () => {
|
||||
const result = normalizeThemeCss(standardThemeCss);
|
||||
expect(result).toContain(".hljs, .ck-content pre.hljs {");
|
||||
});
|
||||
});
|
||||
|
||||
describe("catppuccin-style themes (code-scoped selectors)", () => {
|
||||
const catppuccinCss =
|
||||
"code.hljs{color:#cdd6f4;background:#1e1e2e}" +
|
||||
"code .hljs-keyword{color:#cba6f7}" +
|
||||
"code .hljs-string{color:#a6e3a1}" +
|
||||
"code .hljs-comment{color:#9399b2}";
|
||||
|
||||
it("rewrites 'code.hljs' container to '.hljs'", () => {
|
||||
const result = normalizeThemeCss(catppuccinCss);
|
||||
expect(result).not.toContain("code.hljs");
|
||||
});
|
||||
|
||||
it("rewrites 'code .hljs-*' token selectors to '.hljs .hljs-*'", () => {
|
||||
const result = normalizeThemeCss(catppuccinCss);
|
||||
expect(result).not.toContain("code .hljs-");
|
||||
expect(result).toContain(".hljs .hljs-keyword");
|
||||
expect(result).toContain(".hljs .hljs-string");
|
||||
expect(result).toContain(".hljs .hljs-comment");
|
||||
});
|
||||
|
||||
it("adds CKEditor specificity to .hljs container rule", () => {
|
||||
const result = normalizeThemeCss(catppuccinCss);
|
||||
expect(result).toContain(".hljs, .ck-content pre.hljs {");
|
||||
});
|
||||
|
||||
it("preserves color values", () => {
|
||||
const result = normalizeThemeCss(catppuccinCss);
|
||||
expect(result).toContain("#cdd6f4");
|
||||
expect(result).toContain("#1e1e2e");
|
||||
expect(result).toContain("#cba6f7");
|
||||
expect(result).toContain("#a6e3a1");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,22 @@ const themeDefinitions: Record<string, Theme> = {
|
||||
name: "Brown Paper (Light)",
|
||||
load: () => import("highlight.js/styles/brown-paper.css?raw")
|
||||
},
|
||||
"catppuccin-latte": {
|
||||
name: "Catppuccin Latte (Light)",
|
||||
load: () => import("@catppuccin/highlightjs/css/catppuccin-latte.css?raw")
|
||||
},
|
||||
"catppuccin-frappe": {
|
||||
name: "Catppuccin Frappé (Dark)",
|
||||
load: () => import("@catppuccin/highlightjs/css/catppuccin-frappe.css?raw")
|
||||
},
|
||||
"catppuccin-macchiato": {
|
||||
name: "Catppuccin Macchiato (Dark)",
|
||||
load: () => import("@catppuccin/highlightjs/css/catppuccin-macchiato.css?raw")
|
||||
},
|
||||
"catppuccin-mocha": {
|
||||
name: "Catppuccin Mocha (Dark)",
|
||||
load: () => import("@catppuccin/highlightjs/css/catppuccin-mocha.css?raw")
|
||||
},
|
||||
"codepen-embed": {
|
||||
name: "CodePen Embed (Dark)",
|
||||
load: () => import("highlight.js/styles/codepen-embed.css?raw")
|
||||
|
||||
@@ -46,6 +46,7 @@ function configurePdfViewerOptions() {
|
||||
if (event.detail?.source === window && window.PDFViewerApplicationOptions) {
|
||||
window.PDFViewerApplicationOptions.set("disablePreferences", true);
|
||||
window.PDFViewerApplicationOptions.set("enableHighlightFloatingButton", true);
|
||||
window.PDFViewerApplicationOptions.set("enableComment", true);
|
||||
}
|
||||
};
|
||||
if (window.parent && window.parent !== window) {
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"devDependencies": {
|
||||
"@digitak/esrun": "3.2.26",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"@typescript-eslint/parser": "8.57.1",
|
||||
"dotenv": "17.3.1",
|
||||
"esbuild": "0.27.4",
|
||||
"eslint": "10.0.3",
|
||||
|
||||
4228
pnpm-lock.yaml
generated
4228
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user