Standalone setup (#9180)

This commit is contained in:
Elian Doran
2026-03-26 19:00:09 +02:00
committed by GitHub
89 changed files with 2003 additions and 8312 deletions

View File

@@ -4,7 +4,7 @@
*/
import { BootstrapDefinition } from '@triliumnext/commons';
import { entity_changes, getContext, getSharedBootstrapItems, getSql, routes } from '@triliumnext/core';
import { entity_changes, getContext, getSharedBootstrapItems, getSql, routes, sql_init } from '@triliumnext/core';
import packageJson from '../../package.json' with { type: 'json' };
import { type BrowserRequest, BrowserRouter } from './browser_router';
@@ -178,10 +178,23 @@ function apiResultHandler(_req: any, res: ResultHandlerResponse, result: unknown
}
/**
* No-op auth middleware for standalone — there's no authentication.
* No-op middleware stubs for standalone mode.
*
* In a browser context there is no network authentication, rate limiting,
* or multi-user access, so all auth/rate-limit middleware is a no-op.
*
* `checkAppNotInitialized` still guards setup routes: if the database is
* already initialised the middleware throws so the route handler is never
* reached (mirrors the server behaviour).
*/
function checkApiAuth() {
// No authentication in standalone mode.
function noopMiddleware() {
// No-op.
}
function checkAppNotInitialized() {
if (sql_init.isDbInitialized()) {
throw new Error("App already initialized.");
}
}
/**
@@ -206,11 +219,15 @@ export function registerRoutes(router: BrowserRouter): void {
const apiRoute = createApiRoute(router, true);
routes.buildSharedApiRoutes({
route: createRoute(router),
asyncRoute: createRoute(router),
apiRoute,
asyncApiRoute: createApiRoute(router, false),
apiResultHandler,
checkApiAuth,
checkApiAuthOrElectron: checkApiAuth
checkApiAuth: noopMiddleware,
checkApiAuthOrElectron: noopMiddleware,
checkAppNotInitialized,
checkCredentials: noopMiddleware,
loginRateLimiter: noopMiddleware
});
apiRoute('get', '/bootstrap', bootstrapRoute);
@@ -220,11 +237,22 @@ export function registerRoutes(router: BrowserRouter): void {
apiRoute("get", "/api/system-checks", () => ({ isCpuArchMismatch: false }));
}
function bootstrapRoute() {
function bootstrapRoute(): BootstrapDefinition {
const assetPath = ".";
const isDbInitialized = sql_init.isDbInitialized();
const commonItems = getSharedBootstrapItems(assetPath, isDbInitialized);
if (!isDbInitialized) {
return {
...commonItems,
isStandalone: true,
baseApiUrl: "../api/",
};
}
return {
...getSharedBootstrapItems(assetPath),
...commonItems,
appPath: assetPath,
device: false, // Let the client detect device type.
csrfToken: "dummy-csrf-token",
@@ -248,7 +276,7 @@ function bootstrapRoute() {
instanceName: null,
appCssNoteIds: [],
TRILIUM_SAFE_MODE: false
} satisfies BootstrapDefinition;
};
}
/**

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
import type { PlatformProvider } from "@triliumnext/core";
export default class StandalonePlatformProvider implements PlatformProvider {
crash(message: string): void {
console.error("[Standalone] FATAL:", message);
self.postMessage({
type: "FATAL_ERROR",
message
});
}
}

View File

@@ -51,20 +51,27 @@ export default class FetchRequestProvider implements RequestProvider {
if ([200, 201, 204].includes(response.status)) {
const text = await response.text();
return text.trim() ? JSON.parse(text) : null;
} else {
const text = await response.text();
let errorMessage: string;
try {
const json = JSON.parse(text);
errorMessage = json?.message || "";
} catch {
errorMessage = text.substring(0, 100);
}
throw new Error(`Request to ${opts.method} ${opts.url} failed, error: ${response.status} ${response.statusText} ${errorMessage}`);
}
const text = await response.text();
let errorMessage: string;
try {
const json = JSON.parse(text);
errorMessage = json?.message || "";
} catch {
errorMessage = text.substring(0, 100);
}
throw new Error(`${response.status} ${opts.method} ${opts.url}: ${errorMessage}`);
} catch (e: any) {
if (e.name === "AbortError") {
throw new Error(`Request to ${opts.method} ${opts.url} failed, error: timeout after ${opts.timeout}ms`);
throw new Error(`${opts.method} ${opts.url} failed, error: timeout after ${opts.timeout}ms`);
}
if (e instanceof TypeError && e.message === "Failed to fetch") {
const isCrossOrigin = !opts.url.startsWith(location.origin);
if (isCrossOrigin) {
throw new Error(`Request to ${opts.url} was blocked. The server may not allow requests from this origin (CORS), or it may be unreachable.`);
}
throw new Error(`Request to ${opts.url} failed. The server may be unreachable.`);
}
throw e;
} finally {
@@ -78,7 +85,7 @@ export default class FetchRequestProvider implements RequestProvider {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`Request to GET ${imageUrl} failed, error: ${response.status} ${response.statusText}`);
throw new Error(`${response.status} GET ${imageUrl} failed`);
}
return await response.arrayBuffer();

View File

@@ -1,8 +1,6 @@
import { type BindableValue, default as sqlite3InitModule } from "@sqlite.org/sqlite-wasm";
import type { DatabaseProvider, RunResult, Statement, Transaction } from "@triliumnext/core";
import demoDbSql from "./db.sql?raw";
// Type definitions for SQLite WASM (the library doesn't export these directly)
type Sqlite3Module = Awaited<ReturnType<typeof sqlite3InitModule>>;
type Sqlite3Database = InstanceType<Sqlite3Module["oo1"]["DB"]>;
@@ -431,32 +429,10 @@ export default class BrowserSqlProvider implements DatabaseProvider {
this.opfsDbPath = undefined; // Not using OPFS
this.db.exec("PRAGMA journal_mode = WAL");
// Initialize with demo data for in-memory databases
// (since they won't persist anyway)
this.initializeDemoDatabase();
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] In-memory database created in ${loadTime.toFixed(2)}ms`);
}
/**
* Initialize the database with demo/starter data.
* This should only be called once when creating a new database.
*
* For OPFS databases, this is called automatically only if the database
* doesn't already exist.
*/
initializeDemoDatabase(): void {
this.ensureDb();
console.log("[BrowserSqlProvider] Initializing database with demo data...");
const startTime = performance.now();
this.db!.exec(demoDbSql);
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] Demo data loaded in ${loadTime.toFixed(2)}ms`);
}
loadFromBuffer(buffer: Uint8Array): void {
this.ensureSqlite3();
// SQLite WASM can deserialize a database from a byte array

View File

@@ -2,6 +2,10 @@ import LocalServerWorker from "./local-server-worker?worker";
let localWorker: Worker | null = null;
const pending = new Map();
function showFatalErrorDialog(message: string) {
alert(message);
}
export function startLocalServerWorker() {
if (localWorker) return localWorker;
localWorker = new LocalServerWorker();
@@ -19,6 +23,13 @@ export function startLocalServerWorker() {
localWorker.onmessage = (event) => {
const msg = event.data;
// Handle fatal platform crashes (shown as a dialog to the user)
if (msg?.type === "FATAL_ERROR") {
console.error("[LocalBridge] Fatal error:", msg.message);
showFatalErrorDialog(msg.message);
return;
}
// Handle worker error reports
if (msg?.type === "WORKER_ERROR") {
console.error("[LocalBridge] Worker reported error:", msg.error);

View File

@@ -56,6 +56,7 @@ let WorkerMessagingProvider: typeof import('./lightweight/messaging_provider').d
let BrowserExecutionContext: typeof import('./lightweight/cls_provider').default;
let BrowserCryptoProvider: typeof import('./lightweight/crypto_provider').default;
let FetchRequestProvider: typeof import('./lightweight/request_provider').default;
let StandalonePlatformProvider: typeof import('./lightweight/platform_provider').default;
let translationProvider: typeof import('./lightweight/translation_provider').default;
let createConfiguredRouter: typeof import('./lightweight/browser_routes').createConfiguredRouter;
@@ -81,6 +82,7 @@ async function loadModules(): Promise<void> {
clsModule,
cryptoModule,
requestModule,
platformModule,
translationModule,
routesModule
] = await Promise.all([
@@ -89,6 +91,7 @@ async function loadModules(): Promise<void> {
import('./lightweight/cls_provider.js'),
import('./lightweight/crypto_provider.js'),
import('./lightweight/request_provider.js'),
import('./lightweight/platform_provider.js'),
import('./lightweight/translation_provider.js'),
import('./lightweight/browser_routes.js')
]);
@@ -98,6 +101,7 @@ async function loadModules(): Promise<void> {
BrowserExecutionContext = clsModule.default;
BrowserCryptoProvider = cryptoModule.default;
FetchRequestProvider = requestModule.default;
StandalonePlatformProvider = platformModule.default;
translationProvider = translationModule.default;
createConfiguredRouter = routesModule.createConfiguredRouter;
@@ -132,15 +136,6 @@ async function initialize(): Promise<void> {
if (sqlProvider!.isOpfsAvailable()) {
console.log("[Worker] OPFS available, loading persistent database...");
sqlProvider!.loadFromOpfs("/trilium.db");
// Check if database is initialized (schema exists)
if (!sqlProvider!.isDbInitialized()) {
console.log("[Worker] Database not initialized, loading demo data...");
sqlProvider!.initializeDemoDatabase();
console.log("[Worker] Demo data loaded");
} else {
console.log("[Worker] Existing initialized database loaded");
}
} else {
// Fall back to in-memory database (non-persistent)
console.warn("[Worker] OPFS not available, using in-memory database (data will not persist)");
@@ -151,13 +146,16 @@ async function initialize(): Promise<void> {
console.log("[Worker] Database loaded");
console.log("[Worker] Loading @triliumnext/core...");
const schemaModule = await import("@triliumnext/core/src/assets/schema.sql?raw");
coreModule = await import("@triliumnext/core");
await coreModule.initializeCore({
executionContext: new BrowserExecutionContext(),
crypto: new BrowserCryptoProvider(),
messaging: messagingProvider!,
request: new FetchRequestProvider(),
platform: new StandalonePlatformProvider(),
translations: translationProvider,
schema: schemaModule.default,
dbConfig: {
provider: sqlProvider!,
isReadOnly: false,
@@ -177,8 +175,16 @@ async function initialize(): Promise<void> {
router = createConfiguredRouter();
console.log("[Worker] Router configured");
console.log("[Worker] Initializing becca...");
await coreModule.becca_loader.beccaLoaded;
// initializeDb runs initDbConnection inside an execution context,
// which resolves dbReady — required before beccaLoaded can settle.
coreModule.sql_init.initializeDb();
if (coreModule.sql_init.isDbInitialized()) {
console.log("[Worker] Database already initialized, loading becca...");
await coreModule.becca_loader.beccaLoaded;
} else {
console.log("[Worker] Database not initialized, skipping becca load (will be loaded during DB initialization)");
}
console.log("[Worker] Initialization complete");
} catch (error) {

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 256 256" version="1.1" viewBox="0 0 256 256" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<title>Trilium Notes</title>
<style type="text/css">
.st0{fill:#95C980;}
.st1{fill:#72B755;}
.st2{fill:#4FA52B;}
.st3{fill:#EE8C89;}
.st4{fill:#E96562;}
.st5{fill:#E33F3B;}
.st6{fill:#EFB075;}
.st7{fill:#E99547;}
.st8{fill:#E47B19;}
</style>
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -112,6 +112,8 @@ function loadIcons() {
}
function setBodyAttributes() {
if (!glob.dbInitialized) return;
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
@@ -132,6 +134,11 @@ function setBodyAttributes() {
}
async function loadScripts() {
if (!glob.dbInitialized) {
await import("./setup.js");
return;
}
switch (glob.device) {
case "mobile":
await import("./mobile.js");

View File

@@ -1,11 +1,11 @@
import appContext from "../components/app_context.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FBlob, { type FBlobRow } from "../entities/fblob.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FNote, { type FNoteRow } from "../entities/fnote.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import server from "./server.js";
import appContext from "../components/app_context.js";
import FBlob, { type FBlobRow } from "../entities/fblob.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import type { Froca } from "./froca-interface.js";
import server from "./server.js";
interface SubtreeResponse {
notes: FNoteRow[];
@@ -44,8 +44,9 @@ class FrocaImpl implements Froca {
}
async loadInitialTree() {
const resp = await server.get<SubtreeResponse>("tree");
if (!glob.dbInitialized) return;
const resp = await server.get<SubtreeResponse>("tree");
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
this.#clear();
this.addResp(resp);
@@ -77,7 +78,7 @@ class FrocaImpl implements Froca {
for (const noteRow of noteRows) {
const { noteId } = noteRow;
let note = this.notes[noteId];
const note = this.notes[noteId];
if (note) {
note.update(noteRow);
@@ -240,9 +241,8 @@ class FrocaImpl implements Froca {
console.trace(`Can't find note '${noteId}'`);
return null;
} else {
return this.notes[noteId];
}
return this.notes[noteId];
})
.filter((note) => !!note) as FNote[];
}
@@ -263,9 +263,8 @@ class FrocaImpl implements Froca {
console.trace(`Can't find note '${noteId}'`);
return null;
} else {
return this.notes[noteId];
}
return this.notes[noteId];
})
.filter((note) => !!note) as FNote[];
}
@@ -338,11 +337,10 @@ class FrocaImpl implements Froca {
attachmentRows = await server.getWithSilentNotFound<FAttachmentRow[]>(`attachments/${attachmentId}/all`);
} catch (e: any) {
if (silentNotFoundError) {
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message);
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ${e.message}`);
return null;
} else {
throw e;
}
throw e;
}
const attachments = this.processAttachmentRows(attachmentRows);

View File

@@ -1,16 +1,17 @@
import options from "./options.js";
import { type Locale, LOCALE_IDS, setDayjsLocale } from "@triliumnext/commons";
import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js";
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
import options from "./options.js";
import server from "./server.js";
let locales: Locale[] | null;
/**
* A deferred promise that resolves when translations are initialized.
*/
export let translationsInitializedPromise = $.Deferred();
export const translationsInitializedPromise = $.Deferred();
export async function initLocale() {
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
@@ -34,7 +35,7 @@ export async function initLocale() {
export function getAvailableLocales() {
if (!locales) {
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
throw new Error("Tried to load list of locales, but localization is not yet initialized.");
}
return locales;

View File

@@ -905,6 +905,10 @@ export function getErrorMessage(e: unknown) {
}
export function replaceHtmlEscapedSlashes(str: string) {
return str.replace(/&#x2F;/g, "/");
}
/**
* Handles left or right placement of e.g. tooltips in case of right-to-left languages. If the current language is a RTL one, then left and right are swapped. Other directions are unaffected.
* @param placement a string optionally containing a "left" or "right" value.

View File

@@ -1,13 +1,14 @@
import utils from "./utils.js";
import toastService from "./toast.js";
import server from "./server.js";
import options from "./options.js";
import frocaUpdater from "./froca_updater.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { EntityChange } from "../server_types.js";
import { WebSocketMessage } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import type { EntityChange } from "../server_types.js";
import frocaUpdater from "./froca_updater.js";
import { t } from "./i18n.js";
import options from "./options.js";
import server from "./server.js";
import toastService from "./toast.js";
import toast from "./toast.js";
import utils from "./utils.js";
type MessageHandler = (message: WebSocketMessage) => void;
let messageHandlers: MessageHandler[] = [];
@@ -179,7 +180,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) {
return new Promise<void>((res, rej) => {
entityChangeIdReachedListeners.push({
desiredEntityChangeId: desiredEntityChangeId,
desiredEntityChangeId,
resolvePromise: res,
start: Date.now()
});
@@ -285,6 +286,7 @@ async function sendPing() {
setTimeout(() => {
if (glob.device === "print") return;
if (!glob.dbInitialized) return;
if (glob.isStandalone) {
// In standalone mode, listen for messages from the local worker via custom event

420
apps/client/src/setup.css Normal file
View File

@@ -0,0 +1,420 @@
html,
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
body.setup {
margin: 0;
padding: 0;
&>.setup-outer-wrapper {
width: 100dvw;
height: 100dvh;
body:not(.electron) & {
@media (min-width: 700px) {
background:
radial-gradient(ellipse at 20% 50%, rgba(99, 102, 241, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(168, 85, 247, 0.25) 0%, transparent 50%),
radial-gradient(ellipse at 60% 80%, rgba(59, 130, 246, 0.25) 0%, transparent 50%),
var(--left-pane-background-color);
display: flex;
justify-content: center;
align-items: center;
padding: 2em;
}
}
.setup-container {
background-color: var(--main-background-color);
border-radius: 16px;
padding: 2em;
flex-direction: column;
gap: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
position: relative;
height: 100%;
@media (min-width: 700px) {
display: flex;
width: 750px;
height: 650px;
top: unset;
overflow: hidden;
}
.setup-options {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
.setup-option-card {
padding: 1.5em;
cursor: pointer;
display: flex;
align-items: center;
gap: 1rem;
&.disabled {
cursor: not-allowed;
opacity: 0.5;
background-color: transparent;
border-color: var(--main-border-color)
}
&:not(.disabled):hover {
background-color: var(--card-background-hover-color);
filter: contrast(105%);
transition: background-color .2s ease-out;
}
.tn-icon {
font-size: 2.5em;
color: var(--muted-text-color);
}
h3 {
font-size: 1.5em;
font-weight: normal;
}
p:last-of-type {
margin-bottom: 0;
color: var(--muted-text-color);
}
}
}
}
.page {
display: flex;
flex-direction: column;
height: 100%;
padding: 2em;
overflow: auto;
>.back-button {
position: absolute;
top: 2em;
left: 2em;
color: var(--muted-text-color);
.tn-icon {
margin-right: 0.4em;
}
}
>main {
flex: 1;
display: flex;
flex-direction: column;
padding-top: 1em;
min-height: 0;
}
&.contentless {
justify-content: center;
align-items: center;
}
>footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
border-top: 1px solid var(--main-border-color);
padding-top: 1rem;
margin-inline: -2em;
padding-inline: 2em;
}
>.page-error {
position: absolute;
top: 0;
left: 0;
right: 0;
background: var(--admonition-caution-accent-color);
z-index: 1;
margin: 0;
border-radius: 0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
padding-right: 2.5em;
button {
position: absolute;
top: 0.5em;
right: 0.5em;
}
}
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 80%;
margin-inline: auto;
.form-group {
margin-bottom: 0;
}
.admonition {
margin: 0;
}
}
.form-item-with-icon {
display: flex;
align-items: center;
gap: 0.5rem;
.tn-icon {
font-size: 1.5em;
color: var(--muted-text-color);
}
}
}
.sync-illustration {
display: flex;
justify-content: center;
margin-top: 1.5em;
margin-bottom: 1.5rem;
.tn-icon {
font-size: 3em;
color: var(--muted-text-color);
}
>div {
display: flex;
flex-direction: column;
text-align: center;
gap: 0.5rem;
line-height: 1;
font-size: 0.85rem;
}
.sync-illustration-arrows {
width: 60px;
height: 3em;
position: relative;
&::after {
content: "";
position: absolute;
border: 2px dashed var(--main-border-color);
top: 1.5em;
left: 0;
right: 0;
}
}
}
.illustration-icon {
font-size: 4em;
text-align: center;
color: var(--muted-text-color);
opacity: 0.6;
margin-block: 1rem;
}
.illustration-logo {
width: 96px;
height: 96px;
margin: auto;
}
h1 {
font-size: 1.4em;
text-align: center;
}
h1 + p {
text-align: center;
color: var(--muted-text-color);
margin-bottom: 0;
}
.tooltip {
z-index: 15 !important;
}
}
body.setup.background-effects,
body.setup.background-effects .setup-container {
background: transparent;
}
/* macOS: draggable title bar region and traffic light buttons */
body.setup.platform-darwin {
.drag-region {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40px;
-webkit-app-region: drag;
z-index: 10;
}
.back-button {
-webkit-app-region: no-drag;
z-index: 11;
}
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Slide transitions */
.slide-page {
position: absolute;
inset: 0;
}
.slide-out-forward,
.slide-out-backward,
.slide-in-forward,
.slide-in-backward {
animation-duration: 0.35s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
.slide-out-forward {
animation-name: slide-out-left;
}
.slide-out-backward {
animation-name: slide-out-right;
}
.slide-in-forward {
animation-name: slide-in-right;
}
.slide-in-backward {
animation-name: slide-in-left;
}
.page.select-language {
.dropdownWrapper {
padding-bottom: 2em;
width: 80%;
margin: auto;
}
.dropdownWrapper,
.dropdown,
.dropdown-menu {
height: 100%;
}
.dropdown-menu {
box-sizing: border-box;
overflow: auto;
}
}
.page.sync-from-desktop {
.card-columns {
display: flex;
flex-direction: row;
gap: 1.5rem;
}
.sync-from-desktop-waiting {
margin-top: 2rem;
text-align: center;
.main {
font-size: 1.35em;
}
.subtle {
color: var(--muted-text-color);
}
}
.ip-addresses {
min-width: 250px;
user-select: text;
display: flex;
flex-direction: column;
font-family: var(--monospace-font-family);
font-size: 0.9em;
.tn-card-body {
overflow: auto;
padding-bottom: 0.5em;
> :first-child {
font-weight: bold;
}
}
}
}
.page.sync-in-progress {
.sync-progress {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
progress {
width: 100%;
height: 1rem;
border-radius: 0.5rem;
overflow: hidden;
appearance: none;
&::-webkit-progress-bar {
background-color: var(--main-border-color);
}
&::-webkit-progress-value {
background-color: var(--main-text-color);
transition: width 0.2s ease-out;
}
}
span {
font-size: 0.85rem;
color: var(--muted-text-color);
min-width: 2.5em;
text-align: right;
}
}
}
@keyframes slide-out-left {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-out-right {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slide-in-left {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}

View File

@@ -1,204 +0,0 @@
import "jquery";
import utils from "./services/utils.js";
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 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 sections: Record<SetupStep, HTMLElement>;
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.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)
};
}
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();
});
}
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 = "";
}
private async selectSetupType() {
if (this.setupType === "new-document") {
this.setStep("new-document-in-progress");
await $.post("api/setup/new-document");
window.location.replace("./setup");
return;
}
if (this.setupType) {
this.setStep(this.setupType);
}
}
private back() {
this.setStep("setup-type");
this.setupType = "";
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");
return;
}
if (!password) {
showAlert("Password can't be empty");
return;
}
// not using server.js because it loads too many dependencies
const resp = await $.post("api/setup/sync-from-server", {
syncServerHost,
syncProxy,
password
});
if (resp.result === "success") {
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.setupTypeNextButton.disabled = !this.setupType;
}
private getSelectedSetupType(): SetupType {
return (this.setupTypeInputs.find((input) => input.checked)?.value ?? "") as SetupType;
}
private startSyncPolling() {
if (this.syncPollIntervalId !== null) {
return;
}
this.syncPollIntervalId = window.setInterval(checkOutstandingSyncs, 1000);
}
}
async function checkOutstandingSyncs() {
const { outstandingPullCount, initialized } = await $.get("api/sync/stats");
if (initialized) {
if (utils.isElectron()) {
const remote = utils.dynamicRequire("@electron/remote");
remote.app.relaunch();
remote.app.exit(0);
} else {
utils.reloadFrontendApp();
}
} else {
$("#outstanding-syncs").html(outstandingPullCount);
}
}
function showAlert(message: string) {
$("#alert").text(message);
$("#alert").show();
}
function hideAlert() {
$("#alert").hide();
}
function getSyncInProgress() {
const el = document.getElementById("syncInProgress");
if (!el || !(el instanceof HTMLMetaElement)) return false;
return !!parseInt(el.content);
}
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", (event) => {
const rootNode = document.getElementById("setup-dialog");
if (!rootNode || !(rootNode instanceof HTMLElement)) return;
new SetupController(rootNode, getSyncInProgress()).init();
});

524
apps/client/src/setup.tsx Normal file
View File

@@ -0,0 +1,524 @@
import "./setup.css";
import { LOCALES, SetupSyncFromServerResponse } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChildren, render } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import logo from "./assets/icon-color.svg?url";
import { initLocale, t } from "./services/i18n";
import server from "./services/server";
import { isElectron, replaceHtmlEscapedSlashes } from "./services/utils";
import ActionButton from "./widgets/react/ActionButton";
import Admonition from "./widgets/react/Admonition";
import Button from "./widgets/react/Button";
import { Card, CardFrame, CardSection } from "./widgets/react/Card";
import FormGroup from "./widgets/react/FormGroup";
import FormList, { FormListItem } from "./widgets/react/FormList";
import FormTextBox from "./widgets/react/FormTextBox";
import Icon from "./widgets/react/Icon";
async function main() {
await initLocale();
const bodyWrapper = document.createElement("div");
bodyWrapper.classList.add("setup-outer-wrapper");
document.body.classList.add("setup");
if (isElectron()) {
document.body.classList.add("electron", `platform-${window.process.platform}`, "background-effects");
}
render(<App />, bodyWrapper);
document.body.replaceChildren(bodyWrapper);
}
type State = "selectLanguage" | "firstOptions" | "createNewDocumentOptions" | "createNewDocumentWithDemo" | "createNewDocumentEmpty" | "syncFromDesktop" | "syncFromServer" | "syncFromServerInProgress" | "syncFromDesktopInProgress" | "syncFailed";
const STATE_ORDER: State[] = ["selectLanguage", "firstOptions", "createNewDocumentOptions", "createNewDocumentWithDemo", "createNewDocumentEmpty", "syncFromDesktop", "syncFromServer", "syncFromServerInProgress", "syncFromDesktopInProgress", "syncFailed"];
function renderState(state: State, setState: (state: State) => void) {
switch (state) {
case "selectLanguage": return <SelectLanguage setState={setState} />;
case "firstOptions": return <SetupOptions setState={setState} />;
case "createNewDocumentOptions": return <CreateNewDocumentOptions setState={setState} />;
case "createNewDocumentWithDemo": return <CreateNewDocumentInProgress withDemo />;
case "createNewDocumentEmpty": return <CreateNewDocumentInProgress />;
case "syncFromServer": return <SyncFromServer setState={setState} />;
case "syncFromDesktop": return <SyncFromDesktop setState={setState} />;
case "syncFromServerInProgress": return <SyncInProgress device="server" />;
case "syncFromDesktopInProgress": return <SyncInProgress device="desktop" />;
default: return null;
}
}
function App() {
const [state, setState] = useState<State>("selectLanguage");
const [prevState, setPrevState] = useState<State | null>(null);
const [transitioning, setTransitioning] = useState(false);
const prevStateRef = useRef<State>(state);
function handleSetState(newState: State) {
setPrevState(prevStateRef.current);
prevStateRef.current = newState;
setTransitioning(true);
setState(newState);
}
const direction = prevState !== null
? STATE_ORDER.indexOf(state) > STATE_ORDER.indexOf(prevState) ? "forward" : "backward"
: "forward";
return (
<div class="setup-container">
<div class="drag-region" />
{transitioning && prevState !== null && (
<div
class={`slide-page slide-out-${direction}`}
onAnimationEnd={() => {
setTransitioning(false);
setPrevState(null);
}}
>
{renderState(prevState, handleSetState)}
</div>
)}
<div class={`slide-page ${transitioning ? `slide-in-${direction}` : "slide-current"}`} key={state}>
{renderState(state, handleSetState)}
</div>
</div>
);
}
function SelectLanguage({ setState }: { setState: (state: State) => void }) {
const { t, i18n } = useTranslation();
const [ currentLocale, setCurrentLocale ] = useState(i18n.language);
const filteredLocales = useMemo(() => LOCALES.filter(l => !l.contentOnly), []);
return (
<SetupPage
title={t("setup.language")}
className="select-language"
illustration={<Icon icon="bx bx-globe" className="illustration-icon" />}
footer={<Button text={t("setup.continue")} kind="primary" onClick={() => setState("firstOptions")} />}
>
<FormList onSelect={async (id) => {
await i18n.changeLanguage(id);
setCurrentLocale(id);
}}>
{filteredLocales.map(locale => (
<FormListItem key={locale.id} value={locale.id} active={locale.id === currentLocale}>{locale.name}</FormListItem>
))}
</FormList>
</SetupPage>
);
}
function SetupOptions({ setState }: { setState: (state: State) => void }) {
return (
<SetupPage
title={t("setup.heading")}
className="setup-options-container"
illustration={<img src={logo} alt="Setup illustration" className="illustration-logo" />}
onBack={() => setState("selectLanguage")}
>
<div class="setup-options">
<SetupOptionCard
icon="bx bx-file-blank"
title={t("setup.new-document")}
description={t("setup.new-document-description")}
onClick={() => setState("createNewDocumentOptions")}
/>
<SetupOptionCard
icon="bx bx-server"
title={t("setup.sync-from-server")}
description={t("setup.sync-from-server-description")}
onClick={() => setState("syncFromServer")}
/>
<SetupOptionCard
icon="bx bx-desktop"
title={t("setup.sync-from-desktop")}
description={t("setup.sync-from-desktop-description")}
disabled={glob.isStandalone}
onClick={() => setState("syncFromDesktop")}
/>
</div>
</SetupPage>
);
}
type SyncStep = "connecting" | "syncing" | "finalizing";
function getSyncStep(stats: { outstandingPullCount: number; totalPullCount: number | null; initialized: boolean }): SyncStep {
if (stats.initialized) {
return "finalizing"; // will reload momentarily
}
if (stats.totalPullCount !== null && stats.outstandingPullCount > 0) {
return "syncing";
}
if (stats.totalPullCount !== null && stats.outstandingPullCount === 0) {
return "finalizing";
}
return "connecting";
}
function SyncInProgress({ device }: { device: "server" | "desktop" }) {
const stats = useOutstandingSyncInfo();
const step = getSyncStep(stats);
useEffect(() => {
if (stats.initialized) {
onSetupFinished();
}
}, [stats.initialized]);
const steps: { key: SyncStep; label: string }[] = [
{ key: "connecting", label: t("setup.sync-step-connecting") },
{ key: "syncing", label: t("setup.sync-step-syncing") },
{ key: "finalizing", label: t("setup.sync-step-finalizing") }
];
const currentIndex = steps.findIndex((s) => s.key === step);
const syncingDone = currentIndex > steps.findIndex((s) => s.key === "syncing");
let progress = 0;
if (syncingDone) {
progress = 100;
} else if (stats.totalPullCount) {
progress = Math.round(((stats.totalPullCount - stats.outstandingPullCount) / stats.totalPullCount) * 100);
}
return (
<SetupPage
className="sync-in-progress"
illustration={<SyncIllustration targetDevice={device} />}
title={t("setup.sync-in-progress-title")}
>
<Card className="sync-steps">
{steps.map((s, i) => (
<CardSection className={i < currentIndex ? "completed" : i === currentIndex ? "active" : ""} key={s.key}>
<Icon icon={i < currentIndex ? "bx bx-check-circle" : i === currentIndex ? "bx bx-loader-circle bx-spin" : "bx bx-circle"} />{" "}
{s.label}
{s.key === "syncing" && (
<div class="sync-progress">
<progress value={syncingDone ? 1 : stats.totalPullCount! - stats.outstandingPullCount} max={syncingDone ? 1 : stats.totalPullCount!} />
<span>{progress}%</span>
</div>
)}
</CardSection>
))}
</Card>
</SetupPage>
);
}
function useOutstandingSyncInfo() {
const [ outstandingPullCount, setOutstandingPullCount ] = useState(0);
const [ totalPullCount, setTotalPullCount ] = useState<number | null>(null);
const [ initialized, setInitialized ] = useState(false);
async function refresh() {
const resp = await server.get<{ outstandingPullCount: number; totalPullCount: number | null; initialized: boolean }>("sync/stats");
setOutstandingPullCount(resp.outstandingPullCount);
setTotalPullCount(resp.totalPullCount);
setInitialized(resp.initialized);
}
useEffect(() => {
const interval = setInterval(refresh, 1000);
refresh();
return () => clearInterval(interval);
}, []);
return { outstandingPullCount, totalPullCount, initialized };
}
function CreateNewDocumentOptions({ setState }: { setState: (state: State) => void }) {
return (
<SetupPage
className="create-new-document-options"
title={t("setup.create-new-document-options-title")}
illustration={<Icon icon="bx bx-star" className="illustration-icon" />}
onBack={() => setState("firstOptions")}
>
<div class="setup-options">
<SetupOptionCard icon="bx bx-book-open" title={t("setup.create-new-document-options-with-demo")} description={t("setup.create-new-document-options-with-demo-description")} onClick={() => setState("createNewDocumentWithDemo")} />
<SetupOptionCard icon="bx bx-file-blank" title={t("setup.create-new-document-options-empty")} description={t("setup.create-new-document-options-empty-description")} onClick={() => setState("createNewDocumentEmpty")} />
</div>
</SetupPage>
);
}
function CreateNewDocumentInProgress({ withDemo = false }: { withDemo?: boolean }) {
useEffect(() => {
server.post(`setup/new-document${withDemo ? "" : "?skipDemoDb"}`).then(onSetupFinished);
}, [ withDemo ]);
return (
<SetupPage
className="create-new-document"
title={t("setup.create-new-document-title")}
description={t("setup.create-new-document-description")}
illustration={<Icon icon="bx bx-loader-circle bx-spin" className="illustration-icon" />}
/>
);
}
function SyncFromServer({ setState }: { setState: (state: State) => void }) {
const [ syncServerHost, setSyncServerHost ] = useState("");
const [ password, setPassword ] = useState("");
const [ syncProxy, setSyncProxy ] = useState("");
const [ error, setError ] = useState<string | null>(null);
const [ errorId, setErrorId ] = useState(0);
const [ isWrongPassword, setIsWrongPassword ] = useState(false);
const isValid = syncServerHost.trim() !== "" && password !== "";
function raiseError(message: string) {
setError(message);
setErrorId(id => id + 1);
}
async function handleFinishSetup() {
try {
const resp = await server.post<SetupSyncFromServerResponse>("setup/sync-from-server", {
syncServerHost: syncServerHost.trim(),
syncProxy: syncProxy.trim(),
password
});
if (resp.result === "success") {
setState("syncFromServerInProgress");
} else if (resp.error.includes("Incorrect password")) {
setIsWrongPassword(true);
} else {
raiseError(t("setup.sync-failed", { message: resp.error }));
}
} catch (e) {
raiseError(e instanceof Error ? e.message : String(e));
}
}
return (
<SetupPage
className="sync-from-server top-aligned"
title={t("setup.sync-from-server")}
description={t("setup.sync-from-server-page-description")}
illustration={<SyncIllustration targetDevice="server" />}
error={error}
errorId={errorId}
onBack={() => setState("firstOptions")}
footer={<Button text={t("setup.button-finish-setup")} kind="primary" onClick={handleFinishSetup} disabled={!isValid} />}
>
<form>
<Card>
<CardSection>
<FormGroup label={t("setup.server-host")} name="serverHost">
<FormTextBox
placeholder={t("setup.server-host-placeholder")}
currentValue={syncServerHost} onChange={setSyncServerHost}
autocomplete="trilium-sync-server-host"
required
/>
</FormGroup>
</CardSection>
<CardSection>
<FormGroup
label={t("setup.server-password")} name="serverPassword"
error={isWrongPassword ? t("setup.wrong-password") : undefined}
>
<FormTextBox
type="password"
currentValue={password} onChange={setPassword}
autocomplete="trilium-sync-server-password"
required
/>
</FormGroup>
</CardSection>
</Card>
<Card heading={t("setup.advanced-options")}>
<CardSection>
<FormGroup
name="proxyServer"
label={t("setup.proxy-server")}
description={isElectron() ? t("setup.proxy-instruction") : undefined}
>
<FormTextBox placeholder={t("setup.proxy-server-placeholder")} currentValue={syncProxy} onChange={setSyncProxy} />
</FormGroup>
</CardSection>
</Card>
</form>
</SetupPage>
);
}
function SyncFromDesktop({ setState }: { setState: (state: State) => void }) {
const networkAddresses = getNetworkAddresses();
useEffect(() => {
const interval = setInterval(async () => {
const status = await server.get<{ schemaExists: boolean }>("setup/status");
if (status.schemaExists) {
setState("syncFromDesktopInProgress");
}
}, 1000);
return () => clearInterval(interval);
}, [setState]);
return (
<SetupPage
className="sync-from-desktop"
title={t("setup.sync-from-desktop")}
illustration={<SyncIllustration targetDevice="desktop" />}
onBack={() => setState("firstOptions")}
>
<div class="card-columns">
<Card heading="On the other device">
<CardSection>1. {t("setup.sync-from-desktop-step1")}</CardSection>
<CardSection>2. {t("setup.sync-from-desktop-step2")}</CardSection>
<CardSection>3. {t("setup.sync-from-desktop-step3")}</CardSection>
<CardSection>4. {t("setup.sync-from-desktop-step4")}</CardSection>
<CardSection>5. {t("setup.sync-from-desktop-step5")}</CardSection>
</Card>
{networkAddresses.length > 0 && (
<Card heading={t("setup.your-ip-addresses")} className="ip-addresses">
{networkAddresses.map((addr) => (
<CardSection key={addr}>{addr}</CardSection>
))}
</Card>
)}
</div>
<div class="sync-from-desktop-waiting">
<div class="main"><Icon icon="bx bx-loader-circle bx-spin" />{" "} {t("setup.sync-from-desktop-waiting")}</div>
<div class="subtle">{t("setup.sync-from-desktop-warning")}</div>
</div>
</SetupPage>
);
}
function SyncIllustration({ targetDevice }: { targetDevice: "desktop" | "server" }) {
return (
<div class="sync-illustration">
<div>
<Icon icon={isElectron() ? "bx bx-desktop" : "bx bx-globe"} />
{t("setup.sync-illustration-this-device")}
</div>
<div class="sync-illustration-arrows" />
<div>
<Icon icon={targetDevice === "desktop" ? "bx bx-desktop" : "bx bx-server"} />
{targetDevice === "desktop" ? t("setup.sync-illustration-desktop-app") : t("setup.sync-illustration-server")}
</div>
</div>
);
}
function SetupOptionCard({ title, description, icon, onClick, disabled }: { title: string; description: string, icon: string, onClick?: () => void, disabled?: boolean }) {
return (
<CardFrame
className={clsx("setup-option-card", { disabled })}
onClick={disabled ? undefined : onClick}
>
<Icon icon={icon} />
<div>
<h3>{title}</h3>
<p>{description}</p>
</div>
</CardFrame>
);
}
function SetupPage({ title, description, className, illustration, children, footer, error, errorId, onBack }: {
title: string;
description?: string;
error?: string | null;
errorId?: number;
className?: string;
illustration?: ComponentChildren;
children?: ComponentChildren;
footer?: ComponentChildren;
onBack?: () => void;
}) {
const [ showError, setShowError ] = useState(!!error);
useEffect(() => {
if (error) {
setShowError(true);
}
}, [ error, errorId ]);
return (
<div className={clsx("page", className, { "contentless": !children })}>
{onBack && (
<Button
className="back-button"
icon="bx bx-arrow-back"
text={t("setup.button-back")}
onClick={onBack}
kind="lowProfile"
/>
)}
{error && showError && (
<Admonition className="page-error" type="caution">
<ActionButton icon="bx bx-x" text={t("setup.dismiss-error")} onClick={() => setShowError(false)} />
{replaceHtmlEscapedSlashes(error)}
</Admonition>
)}
{illustration}
<h1>{title}</h1>
{description && <p class="page-description">{description}</p>}
{children && <main>
{children}
</main>}
{footer && <footer>{footer}</footer>}
</div>
);
}
function getNetworkAddresses(): string[] {
if (!isElectron()) {
return [`${location.protocol}//${location.host}`];
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const os = require("os") as typeof import("os");
const interfaces = os.networkInterfaces();
const addresses: string[] = [];
for (const nets of Object.values(interfaces)) {
if (!nets) continue;
for (const net of nets) {
if (net.internal) continue;
if (net.family === "IPv6" && net.scopeid !== 0) continue;
addresses.push(net.address);
}
}
// Sort by likelihood of being the local network address.
addresses.sort((a, b) => networkScore(a) - networkScore(b));
return addresses.map((addr) => `${location.protocol}//${addr}:${location.port}`);
}
function networkScore(addr: string): number {
if (addr.startsWith("192.168.")) return 0;
if (addr.startsWith("10.")) return 1;
if (/^172\.(1[6-9]|2\d|3[01])\./.test(addr)) return 2;
if (addr.includes(":")) return 4; // IPv6
return 3;
}
function onSetupFinished() {
if (isElectron()) {
// On Electron we need to use the setup route because it handles the closing of the setup window and opening the main app window.
location.href = "setup";
} else {
location.reload();
}
}
main();

View File

@@ -2230,5 +2230,56 @@
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa"
},
"setup": {
"heading": "Get started with Trilium",
"new-document": "New knowledge base",
"new-document-description": "Start with a clean knowledge base and begin right away.",
"sync-from-desktop": "Connect a desktop app",
"sync-from-desktop-description": "You only have a Trilium desktop app running on another device. This device will sync its data from that desktop app.",
"sync-from-server": "Connect to an existing server",
"sync-from-server-description": "You have a Trilium server running elsewhere (either self-hosted or in the cloud). This device will sync its data from that server.",
"next": "Next",
"init-in-progress": "Document initialization in progress",
"redirecting": "You will be shortly redirected to the application.",
"title": "Setup",
"sync-from-server-page-description": "Enter your server details below to connect your existing workspace.",
"sync-in-progress-title": "Sync in progress",
"sync-in-progress-description": "Your device is now connected and items are being synchronized.",
"button-back": "Back",
"button-finish-setup": "Finish setup",
"sync-step-connecting": "Connecting to server",
"sync-step-syncing": "Syncing data",
"sync-step-finalizing": "Setting up options",
"create-new-document-options-title": "How would you like to start?",
"create-new-document-options-with-demo": "With demo content",
"create-new-document-options-with-demo-description": "Explore Trilium with example content.",
"create-new-document-options-empty": "Empty",
"create-new-document-options-empty-description": "Start with a blank knowledge base. You can import demo notes later.",
"create-new-document-title": "Preparing your knowledge base",
"create-new-document-description": "This will only take a moment.",
"sync-illustration-this-device": "This device",
"sync-illustration-desktop-app": "Your desktop app",
"sync-illustration-server": "Your server",
"sync-from-desktop-step1": "Open your desktop instance of Trilium Notes.",
"sync-from-desktop-step2": "From the Trilium Menu, click Options.",
"sync-from-desktop-step3": "Click on Sync category in the note tree.",
"sync-from-desktop-step4": "Change server instance address to point to one of the addresses on the right and click Save.",
"sync-from-desktop-step5": "Click the \"Test sync\" button to verify connection is successful.",
"sync-from-desktop-warning": "Make sure both devices are on the same network.",
"sync-from-desktop-waiting": "Waiting for connection...",
"advanced-options": "Advanced options",
"sync-failed": "Failed to sync: {{message}}",
"server-host": "Trilium server address",
"server-host-placeholder": "https://<hostname>:<port>",
"server-password": "Password",
"proxy-server": "Proxy server (optional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"proxy-instruction": "If you leave proxy setting blank, system proxy will be used.",
"dismiss-error": "Dismiss error",
"wrong-password": "Incorrect password. Please try again.",
"language": "Language",
"continue": "Continue",
"your-ip-addresses": "Addresses for this device"
}
}

View File

@@ -2137,5 +2137,55 @@
"title_few": "{{count}} taburi",
"title_other": "{{count}} de taburi",
"more_options": "Mai multe opțiuni"
},
"setup": {
"heading": "Începeți cu Trilium",
"new-document": "Nouă bază de cunoștințe",
"new-document-description": "Începeți cu o bază de cunoștințe curată și începeți imediat.",
"sync-from-desktop": "Conectează o aplicație desktop",
"sync-from-desktop-description": "Aveți doar o aplicație Trilium desktop rulând pe un alt dispozitiv. Acest dispozitiv va sincroniza datele de la acea aplicație desktop.",
"sync-from-server": "Conectează-te la un server existent",
"sync-from-server-description": "Aveți un server Trilium care rulează în altă parte (fie auto-găzduit, fie în cloud). Acest dispozitiv va sincroniza datele de la acel server.",
"next": "Următorul",
"init-in-progress": "Inițializarea documentului în curs",
"redirecting": "Veți fi redirecționat în scurt timp către aplicație.",
"title": "Configurare",
"sync-from-server-page-description": "Introduceți detaliile serverului dvs. mai jos pentru a vă conecta la spațiul de lucru existent.",
"sync-in-progress-title": "Sincronizare în curs",
"sync-in-progress-description": "Dispozitivul dvs. este acum conectat și elementele sunt sincronizate.",
"button-back": "Înapoi",
"button-finish-setup": "Finalizează configurarea",
"sync-step-connecting": "Conectare la server",
"sync-step-syncing": "Sincronizare date",
"sync-step-finalizing": "Setarea opțiunilor",
"create-new-document-options-title": "Cum doriți să începeți?",
"create-new-document-options-with-demo": "Cu conținut demonstrativ",
"create-new-document-options-with-demo-description": "Explorează Trilium cu conținut exemplu.",
"create-new-document-options-empty": "Gol",
"create-new-document-options-empty-description": "Începeți cu o bază de cunoștințe goală. Puteți importa notițe demo mai târziu.",
"create-new-document-title": "Pregătirea bazei de cunoștințe",
"create-new-document-description": "Acest proces va dura doar câteva momente.",
"sync-illustration-this-device": "Acest dispozitiv",
"sync-illustration-desktop-app": "Aplicație desktop",
"sync-illustration-server": "Server de sincronizare",
"sync-from-desktop-step1": "Deschideți aplicația Trilium Notes pentru desktop.",
"sync-from-desktop-step2": "Din meniul Trilium, dați clic pe Opțiuni.",
"sync-from-desktop-step3": "Clic pe categoria „Sincronizare”.",
"sync-from-desktop-step4": "Schimbați adresa server-ului către: {{- host}} și apăsați „Salvează”.",
"sync-from-desktop-step5": "Clic pe butonul „Testează sincronizarea” pentru a verifica dacă conexiunea a fost făcută cu succes.",
"sync-from-desktop-final": "După ce ați finalizat acești pași, puteți trece la pasul următor.",
"sync-from-desktop-waiting": "Așteptare pentru conexiune...",
"advanced-options": "Opțiuni avansate",
"sync-failed": "Sincronizare eșuată: {{message}}",
"server-host": "Adresa serverului Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"server-password": "Parolă",
"proxy-server": "Server proxy (opțional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"proxy-instruction": " Dacă lăsați setarea proxy necompletată, va fi utilizat proxy-ul sistemului.",
"dismiss-error": "Închide mesajul de eroare",
"wrong-password": "Parolă greșită. Vă rugăm să încercați din nou.",
"language": "Limbă",
"continue": "Continuă"
}
}

View File

@@ -43,13 +43,13 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
setFullyExpanded(true);
}, 250);
return () => clearTimeout(timeout);
} else {
setFullyExpanded(true);
}
}
setFullyExpanded(true);
} else {
setFullyExpanded(false);
}
}, [expanded, transitionEnabled])
}, [expanded, transitionEnabled]);
return (
<div className={clsx("collapsible", className, {
@@ -58,7 +58,10 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
})}>
<button
className="collapsible-title tn-low-profile"
onClick={() => setExpanded(!expanded)}
onClick={(e) => {
e.preventDefault();
setExpanded(!expanded);
}}
aria-expanded={expanded}
aria-controls={contentId}
>

View File

@@ -1,5 +1,6 @@
import { cloneElement, ComponentChildren, RefObject, VNode } from "preact";
import { CSSProperties } from "preact/compat";
import { useUniqueName } from "./hooks";
interface FormGroupProps {
@@ -8,6 +9,7 @@ interface FormGroupProps {
label?: string;
title?: string;
className?: string;
error?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children: VNode<any>;
description?: string | ComponentChildren;
@@ -15,7 +17,7 @@ interface FormGroupProps {
style?: CSSProperties;
}
export default function FormGroup({ name, label, title, className, children, description, labelRef, disabled, style }: FormGroupProps) {
export default function FormGroup({ name, label, title, className, children, description, labelRef, disabled, style, error }: FormGroupProps) {
const id = useUniqueName(name);
const childWithId = cloneElement(children, { id });
@@ -26,6 +28,7 @@ export default function FormGroup({ name, label, title, className, children, des
{childWithId}
{error && <div><small className="form-text text-danger">{error}</small></div>}
{description && <div><small className="form-text">{description}</small></div>}
</div>
);
@@ -41,4 +44,4 @@ export function FormMultiGroup({ label, children }: { label: string, children: C
{children}
</div>
);
}
}

View File

@@ -87,7 +87,6 @@ export default defineConfig(() => ({
input: {
index: join(__dirname, "index.html"),
login: join(__dirname, "src", "login.ts"),
setup: join(__dirname, "src", "setup.ts"),
set_password: join(__dirname, "src", "set_password.ts"),
runtime: join(__dirname, "src", "runtime.ts"),
print: join(__dirname, "src", "print.tsx")

View File

@@ -33,6 +33,7 @@
"devDependencies": {
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/core": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "14.0.0",
"electron": "41.0.3",

View File

@@ -1,17 +1,24 @@
import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js";
import { t } from "i18next";
import { app, globalShortcut, BrowserWindow } from "electron";
import sqlInit from "@triliumnext/server/src/services/sql_init.js";
import windowService from "@triliumnext/server/src/services/window.js";
import tray from "@triliumnext/server/src/services/tray.js";
import { getLog, initializeCore, sql_init } from "@triliumnext/core";
import ClsHookedExecutionContext from "@triliumnext/server/src/cls_provider.js";
import NodejsCryptoProvider from "@triliumnext/server/src/crypto_provider.js";
import dataDirs from "@triliumnext/server/src/services/data_dir.js";
import options from "@triliumnext/server/src/services/options.js";
import port from "@triliumnext/server/src/services/port.js";
import NodeRequestProvider from "@triliumnext/server/src/services/request.js";
import tray from "@triliumnext/server/src/services/tray.js";
import windowService from "@triliumnext/server/src/services/window.js";
import WebSocketMessagingProvider from "@triliumnext/server/src/services/ws_messaging_provider.js";
import BetterSqlite3Provider from "@triliumnext/server/src/sql_provider.js";
import { app, BrowserWindow,globalShortcut } from "electron";
import electronDebug from "electron-debug";
import electronDl from "electron-dl";
import { PRODUCT_NAME } from "./app-info";
import port from "@triliumnext/server/src/services/port.js";
import { join } from "path";
import fs from "fs";
import { t } from "i18next";
import path, { join } from "path";
import { deferred, LOCALES } from "../../../packages/commons/src";
import { PRODUCT_NAME } from "./app-info";
import DesktopPlatformProvider from "./platform_provider";
async function main() {
const userDataPath = getUserData();
@@ -82,7 +89,7 @@ async function main() {
}
});
await initializeTranslations();
// await initializeTranslations();
const isPrimaryInstance = (await import("electron")).app.requestSingleInstanceLock();
if (!isPrimaryInstance) {
@@ -93,9 +100,55 @@ async function main() {
// this is to disable electron warning spam in the dev console (local development only)
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
const { DOCUMENT_PATH } = (await import("@triliumnext/server/src/services/data_dir.js")).default;
const config = (await import("@triliumnext/server/src/services/config.js")).default;
const dbProvider = new BetterSqlite3Provider();
dbProvider.loadFromFile(DOCUMENT_PATH, config.General.readOnly);
await initializeCore({
dbConfig: {
provider: dbProvider,
isReadOnly: config.General.readOnly,
async onTransactionCommit() {
const ws = (await import("@triliumnext/server/src/services/ws.js")).default;
ws.sendTransactionEntityChangesToAllClients();
},
async onTransactionRollback() {
const cls = (await import("@triliumnext/server/src/services/cls.js")).default;
const becca_loader = (await import("@triliumnext/core")).becca_loader;
const entity_changes = (await import("@triliumnext/server/src/services/entity_changes.js")).default;
const log = (await import("@triliumnext/server/src/services/log")).default;
const entityChangeIds = cls.getAndClearEntityChangeIds();
if (entityChangeIds.length > 0) {
log.info("Transaction rollback dirtied the becca, forcing reload.");
becca_loader.load();
}
// the maxEntityChangeId has been incremented during failed transaction, need to recalculate
entity_changes.recalculateMaxEntityChangeId();
}
},
crypto: new NodejsCryptoProvider(),
request: new NodeRequestProvider(),
executionContext: new ClsHookedExecutionContext(),
messaging: new WebSocketMessagingProvider(),
schema: fs.readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
platform: new DesktopPlatformProvider(),
translations: (await import("@triliumnext/server/src/services/i18n.js")).initializeTranslations,
extraAppInfo: {
nodeVersion: process.version,
dataDirectory: path.resolve(dataDirs.TRILIUM_DATA_DIR)
}
});
const startTriliumServer = (await import("@triliumnext/server/src/www.js")).default;
await startTriliumServer();
console.log("Server loaded");
serverInitializedPromise.resolve();
}
@@ -112,8 +165,8 @@ async function onReady() {
// if db is not initialized -> setup process
// if db is initialized, then we need to wait until the migration process is finished
if (sqlInit.isDbInitialized()) {
await sqlInit.dbReady;
if (sql_init.isDbInitialized()) {
await sql_init.dbReady;
await windowService.createMainWindow(app);
@@ -127,6 +180,7 @@ async function onReady() {
tray.createTray();
} else {
getLog().banner(t("sql_init.db_not_initialized_desktop"));
await windowService.createSetupWindow();
}
@@ -141,7 +195,7 @@ function getElectronLocale() {
// For RTL, we have to force the UI locale to align the window buttons properly.
if (formattingLocale && !correspondingLocale?.rtl) return formattingLocale;
return uiLocale || "en"
return uiLocale || "en";
}
main();

View File

@@ -0,0 +1,9 @@
import { PlatformProvider, t } from "@triliumnext/core";
import electron from "electron";
export default class DesktopPlatformProvider implements PlatformProvider {
crash(message: string): void {
electron.dialog.showErrorBox(t("modals.error_title"), message);
electron.app.exit(1);
}
}

View File

@@ -19,15 +19,11 @@ import config from "./services/config.js";
import log from "./services/log.js";
import openID from "./services/open_id.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
import sql_init from "./services/sql_init.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
export default async function buildApp() {
const app = express();
// Initialize DB
sql_init.initializeDb();
const publicDir = isDev ? path.join(getResourceDir(), "../dist/public") : path.join(getResourceDir(), "public");
const publicAssetsDir = path.join(publicDir, "assets");
const assetsDir = RESOURCE_DIR;

View File

@@ -241,22 +241,6 @@
"password-confirmation": "تاكيد كلمة المرور",
"button": "تعين كلمة المرور"
},
"setup": {
"next": "التالي",
"title": "تثبيت",
"heading": "تثبيت تريليوم للملاحظات",
"init-in-progress": "جار تهيئة المستند"
},
"setup_sync-from-desktop": {
"step6-here": "هنا",
"heading": "مزامنة من سطح المكتب",
"step3": "انقر على صنف المزامنة."
},
"setup_sync-in-progress": {
"outstanding-items-default": "غير متوفر",
"heading": "المزامنة جارية",
"outstanding-items": "عناصر المزامنة المعلقة:"
},
"share_page": {
"parent": "الأصل:",
"child-notes": "الملاحظات الفرعية:",

View File

@@ -15,24 +15,6 @@
"set_password": {
"password": "Contrasenya"
},
"setup": {
"next": "Següent",
"title": "Configuració"
},
"setup_sync-from-desktop": {
"step6-here": "aquí"
},
"setup_sync-from-server": {
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Nota:",
"password": "Contrasenya",
"password-placeholder": "Contrasenya",
"back": "Torna"
},
"setup_sync-in-progress": {
"outstanding-items-default": "N/A"
},
"share_page": {
"parent": "pare:"
},

View File

@@ -123,47 +123,6 @@
"password-confirmation": "密码确认",
"button": "设置密码"
},
"setup": {
"heading": "TriliumNext笔记设置",
"new-document": "我是新用户我想为我的笔记创建一个新的Trilium文档",
"sync-from-desktop": "我已经有一个桌面实例,我想设置与它的同步",
"sync-from-server": "我已经有一个服务器实例,我想设置与它的同步",
"next": "下一步",
"init-in-progress": "文档初始化进行中",
"redirecting": "您将很快被重定向到应用程序。",
"title": "设置"
},
"setup_sync-from-desktop": {
"heading": "从桌面同步",
"description": "此设置需要从桌面实例开始:",
"step1": "打开您的TriliumNext笔记桌面实例。",
"step2": "从Trilium菜单中点击“选项”。",
"step3": "点击“同步”类别。",
"step4": "将服务器实例地址更改为:{{- host}} 并点击保存。",
"step5": "点击“测试同步”按钮以验证连接是否成功。",
"step6": "完成这些步骤后,点击{{- link}}。",
"step6-here": "这里"
},
"setup_sync-from-server": {
"heading": "从服务器同步",
"instructions": "请在下面输入Trilium服务器地址和凭据。这将从服务器下载整个Trilium文档并设置与它的同步。根据文档大小和您的连接的速度这可能需要一段时间。",
"server-host": "Trilium服务器地址",
"server-host-placeholder": "https://<主机名称>:<端口>",
"proxy-server": "代理服务器(可选)",
"proxy-server-placeholder": "https://<主机名称>:<端口>",
"note": "注意:",
"proxy-instruction": "如果您将代理设置留空,将使用系统代理(仅适用于桌面应用)",
"password": "密码",
"password-placeholder": "密码",
"back": "返回",
"finish-setup": "完成设置"
},
"setup_sync-in-progress": {
"heading": "同步中",
"successful": "同步已被正确设置。初始同步完成可能需要一些时间。完成后,您将被重定向到登录页面。",
"outstanding-items": "未完成的同步项目:",
"outstanding-items-default": "无"
},
"share_404": {
"title": "未找到",
"heading": "未找到"

View File

@@ -123,47 +123,6 @@
"password-confirmation": "Passwortbestätigung",
"button": "Passwort festlegen"
},
"setup": {
"heading": "Trilium Notes Setup",
"new-document": "Ich bin ein neuer Benutzer und möchte ein neues Trilium-Dokument für meine Notizen erstellen",
"sync-from-desktop": "Ich habe bereits eine Desktop-Instanz und möchte die Synchronisierung damit einrichten",
"sync-from-server": "Ich habe bereits eine Server-Instanz und möchte die Synchronisierung damit einrichten",
"next": "Weiter",
"init-in-progress": "Dokumenteninitialisierung läuft",
"redirecting": "Du wirst in Kürze zur Anwendung weitergeleitet.",
"title": "Setup"
},
"setup_sync-from-desktop": {
"heading": "Synchronisation vom Desktop",
"description": "Dieses Setup muss von der Desktop-Instanz aus initiiert werden:",
"step1": "Öffne deine Trilium Notes Desktop-Instanz.",
"step2": "Klicke im Trilium-Menü auf Optionen.",
"step3": "Klicke auf die Kategorie Synchronisation.",
"step4": "Ändere die Server-Instanzadresse auf: {{- host}} und klicke auf Speichern.",
"step5": "Klicke auf den Button \"Test-Synchronisation\", um zu überprüfen, ob die Verbindung erfolgreich ist.",
"step6": "Sobald du diese Schritte abgeschlossen hast, klicke auf {{- link}}.",
"step6-here": "hier"
},
"setup_sync-from-server": {
"heading": "Synchronisation vom Server",
"instructions": "Bitte gib unten die Trilium-Server-Adresse und die Zugangsdaten ein. Dies wird das gesamte Trilium-Dokument vom Server herunterladen und die Synchronisation einrichten. Je nach Dokumentgröße und Verbindungsgeschwindigkeit kann dies eine Weile dauern.",
"server-host": "Trilium Server-Adresse",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Proxy-Server (optional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Hinweis:",
"proxy-instruction": "Wenn du die Proxy-Einstellung leer lässt, wird der System-Proxy verwendet (gilt nur für die Desktop-Anwendung)",
"password": "Passwort",
"password-placeholder": "Passwort",
"back": "Zurück",
"finish-setup": "Setup abschließen"
},
"setup_sync-in-progress": {
"heading": "Synchronisation läuft",
"successful": "Die Synchronisation wurde erfolgreich eingerichtet. Es wird eine Weile dauern, bis die erste Synchronisation abgeschlossen ist. Sobald dies erledigt ist, wirst du zur Anmeldeseite weitergeleitet.",
"outstanding-items": "Ausstehende Synchronisationselemente:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Nicht gefunden",
"heading": "Nicht gefunden"

View File

@@ -220,47 +220,6 @@
"password-confirmation": "Password confirmation",
"button": "Set password"
},
"setup": {
"heading": "Trilium Notes setup",
"new-document": "I'm a new user, and I want to create a new Trilium document for my notes",
"sync-from-desktop": "I have a desktop instance already, and I want to set up sync with it",
"sync-from-server": "I have a server instance already, and I want to set up sync with it",
"next": "Next",
"init-in-progress": "Document initialization in progress",
"redirecting": "You will be shortly redirected to the application.",
"title": "Setup"
},
"setup_sync-from-desktop": {
"heading": "Sync from Desktop",
"description": "This setup needs to be initiated from the desktop instance:",
"step1": "Open your desktop instance of Trilium Notes.",
"step2": "From the Trilium Menu, click Options.",
"step3": "Click on Sync category.",
"step4": "Change server instance address to: {{- host}} and click Save.",
"step5": "Click \"Test sync\" button to verify connection is successful.",
"step6": "Once you've completed these steps, click {{- link}}.",
"step6-here": "here"
},
"setup_sync-from-server": {
"heading": "Sync from Server",
"instructions": "Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.",
"server-host": "Trilium server address",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Proxy server (optional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Note:",
"proxy-instruction": "If you leave proxy setting blank, system proxy will be used (applies to the desktop application only)",
"password": "Password",
"password-placeholder": "Password",
"back": "Back",
"finish-setup": "Finish setup"
},
"setup_sync-in-progress": {
"heading": "Sync in progress",
"successful": "Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.",
"outstanding-items": "Outstanding sync items:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Not found",
"heading": "Not found"

View File

@@ -123,47 +123,6 @@
"password-confirmation": "Confirmación de contraseña",
"button": "Establecer contraseña"
},
"setup": {
"heading": "Configuración de Trilium Notes",
"new-document": "Soy un usuario nuevo y quiero crear un nuevo documento de Trilium para mis notas",
"sync-from-desktop": "Ya tengo una instancia de escritorio y quiero configurar la sincronización con ella",
"sync-from-server": "Ya tengo una instancia de servidor y quiero configurar la sincronización con ella",
"next": "Siguiente",
"init-in-progress": "Inicialización del documento en curso",
"redirecting": "En breve será redirigido a la aplicación.",
"title": "Configuración"
},
"setup_sync-from-desktop": {
"heading": "Sincronizar desde el escritorio",
"description": "Esta configuración debe iniciarse desde la instancia de escritorio:",
"step1": "Abra su instancia de escritorio de Trilium Notes.",
"step2": "En el menú Trilium, dé clic en Opciones.",
"step3": "Dé clic en la categoría Sincronizar.",
"step4": "Cambie la dirección de la instancia del servidor a: {{- host}} y dé clic en Guardar.",
"step5": "Dé clic en el botón \"Probar sincronización\" para verificar que la conexión fue exitosa.",
"step6": "Una vez que haya completado estos pasos, dé clic en {{- link}}.",
"step6-here": "aquí"
},
"setup_sync-from-server": {
"heading": "Sincronización desde el servidor",
"instructions": "Por favor, ingrese la dirección y las credenciales del servidor Trilium a continuación. Esto descargará todo el documento de Trilium desde el servidor y configurará la sincronización. Dependiendo del tamaño del documento y de la velocidad de su conexión, esto puede tardar un poco.",
"server-host": "Dirección del servidor Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Servidor proxy (opcional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Nota:",
"proxy-instruction": "Si deja la configuración de proxy en blanco, se utilizará el proxy del sistema (aplica únicamente a la aplicación de escritorio)",
"password": "Contraseña",
"password-placeholder": "Contraseña",
"back": "Atrás",
"finish-setup": "Finalizar la configuración"
},
"setup_sync-in-progress": {
"heading": "Sincronización en progreso",
"successful": "La sincronización se ha configurado correctamente. La sincronización inicial tardará algún tiempo en finalizar. Una vez hecho esto, será redirigido a la página de inicio de sesión.",
"outstanding-items": "Elementos de sincronización destacados:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "No encontrado",
"heading": "No encontrado"

View File

@@ -123,47 +123,6 @@
"password-confirmation": "Confirmation du mot de passe",
"button": "Définir le mot de passe"
},
"setup": {
"heading": "Configuration de Trilium Notes",
"new-document": "Je suis un nouvel utilisateur et je souhaite créer un nouveau document Trilium pour mes notes",
"sync-from-desktop": "J'ai déjà l'application de bureau et je souhaite configurer la synchronisation avec celle-ci",
"sync-from-server": "J'ai déjà un serveur et je souhaite configurer la synchronisation avec celui-ci",
"next": "Suivant",
"init-in-progress": "Initialisation du document en cours",
"redirecting": "Vous serez bientôt redirigé vers l'application.",
"title": "Configuration"
},
"setup_sync-from-desktop": {
"heading": "Synchroniser depuis une application de bureau",
"description": "Cette procédure doit être réalisée depuis l'application de bureau :",
"step1": "Ouvrez l'application Trilium Notes.",
"step2": "Dans le menu Trilium, cliquez sur Options.",
"step3": "Cliquez sur la catégorie Synchroniser.",
"step4": "Remplacez l'adresse de l'instance de serveur par : {{- host}} et cliquez sur Enregistrer.",
"step5": "Cliquez sur le bouton 'Tester la synchronisation' pour vérifier que la connexion fonctionne.",
"step6": "Une fois que vous avez terminé ces étapes, cliquez sur {{- link}}.",
"step6-here": "ici"
},
"setup_sync-from-server": {
"heading": "Synchroniser depuis le serveur",
"instructions": "Veuillez saisir l'adresse du serveur Trilium et les informations d'identification ci-dessous. Cela téléchargera l'intégralité du document Trilium à partir du serveur et configurera la synchronisation avec celui-ci. En fonction de la taille du document et de votre vitesse de connexion, cela peut prendre un plusieurs minutes.",
"server-host": "Adresse du serveur Trilium",
"server-host-placeholder": "https://<nom d'hôte>:<port>",
"proxy-server": "Serveur proxy (facultatif)",
"proxy-server-placeholder": "https://<nom d'hôte>:<port>",
"note": "Note :",
"proxy-instruction": "Si vous laissez le paramètre de proxy vide, le proxy du système sera utilisé (s'applique uniquement à l'application de bureau)",
"password": "Mot de passe",
"password-placeholder": "Mot de passe",
"back": "Retour",
"finish-setup": "Terminer"
},
"setup_sync-in-progress": {
"heading": "Synchronisation en cours",
"successful": "La synchronisation a été correctement configurée. La synchronisation initiale prendra un certain temps. Une fois terminée, vous serez redirigé vers la page de connexion.",
"outstanding-items": "Éléments de synchronisation exceptionnels :",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Page non trouvée",
"heading": "Page non trouvée"

View File

@@ -220,47 +220,6 @@
"password-confirmation": "Deimhniú pasfhocail",
"button": "Socraigh pasfhocal"
},
"setup": {
"heading": "Socrú Trilium Notes",
"new-document": "Is úsáideoir nua mé, agus ba mhaith liom doiciméad Trilium nua a chruthú do mo nótaí",
"sync-from-desktop": "Tá cás deisce agam cheana féin, agus ba mhaith liom sioncrónú a shocrú leis",
"sync-from-server": "Tá sampla freastalaí agam cheana féin, agus ba mhaith liom sioncrónú a shocrú leis",
"next": "Ar Aghaidh",
"init-in-progress": "Túsú doiciméad ar siúl",
"redirecting": "Atreorófar chuig an bhfeidhmchlár thú go luath.",
"title": "Socrú"
},
"setup_sync-from-desktop": {
"heading": "Sioncrónaigh ón Deasc",
"description": "Ní mór an socrú seo a thionscnamh ón deasc:",
"step1": "Oscail sampla de Trilium Notes ar do dheasc.",
"step2": "Ón Roghchlár Trilium, cliceáil Roghanna.",
"step3": "Cliceáil ar an gcatagóir Sioncrónaigh.",
"step4": "Athraigh seoladh an fhreastalaí go: {{- host}} agus cliceáil Sábháil.",
"step5": "Cliceáil an cnaipe \"Tástáil sioncrónaithe\" chun a fhíorú go bhfuil an nasc rathúil.",
"step6": "Nuair a bheidh na céimeanna seo críochnaithe agat, cliceáil {{- link}}.",
"step6-here": "anseo"
},
"setup_sync-from-server": {
"heading": "Sioncrónaigh ón bhFreastalaí",
"instructions": "Cuir isteach seoladh agus dintiúir freastalaí Trilium thíos le do thoil. Íoslódálfaidh sé seo an doiciméad Trilium iomlán ón bhfreastalaí agus socróidh sé sioncrónú leis. Ag brath ar mhéid an doiciméid agus luas do nasc, d'fhéadfadh sé seo tamall a thógáil.",
"server-host": "Seoladh freastalaí Trilium",
"server-host-placeholder": "https://<ainm óstach>:<port>",
"proxy-server": "Freastalaí seachfhreastalaí (roghnach)",
"proxy-server-placeholder": "https://<ainm óstach>:<port>",
"note": "Nóta:",
"proxy-instruction": "Má fhágann tú an socrú seachfhreastalaí bán, úsáidfear seachfhreastalaí an chórais (baineann sé leis an bhfeidhmchlár deisce amháin)",
"password": "Pasfhocal",
"password-placeholder": "Pasfhocal",
"back": "Ar ais",
"finish-setup": "Críochnaigh an socrú"
},
"setup_sync-in-progress": {
"heading": "Sioncrónú ar siúl",
"successful": "Tá an sioncrónú socraithe i gceart. Tógfaidh sé tamall go mbeidh an sioncrónú tosaigh críochnaithe. Nuair a bheidh sé déanta, atreorófar chuig an leathanach logála isteach thú.",
"outstanding-items": "Míreanna sioncrónaithe gan réiteach:",
"outstanding-items-default": "N/B"
},
"share_404": {
"title": "Níor aimsíodh",
"heading": "Níor aimsíodh"

View File

@@ -323,47 +323,6 @@
"password-confirmation": "Conferma della password",
"button": "Imposta password"
},
"setup": {
"heading": "Configurazione di Trilium Notes",
"new-document": "Sono un nuovo utente, e desidero creare un nuovo documento Trilium per le mie note",
"sync-from-desktop": "Ho già una istanza desktop, e desidero configurare la sincronizzazione con quest'ultima",
"sync-from-server": "Ho già una istanza server, e desidero configurare la sincronizzazione con quest'ultima",
"next": "Avanti",
"init-in-progress": "Inizializzazione del documento in corso",
"redirecting": "Sarai reindirizzato a breve all'applicazione.",
"title": "Configurazione"
},
"setup_sync-from-desktop": {
"heading": "Sincronizza dal desktop",
"description": "Questa configurazione deve essere iniziata dalla istanza desktop:",
"step1": "Apri la tua istanza desktop di Trilium Notes.",
"step2": "Dal menù di Trilium, seleziona \"Opzioni\".",
"step3": "Clicca sulla categoria \"Sincronizzazione\".",
"step4": "Imposta \"{{- host}}\" come l'indirizzo dell'istanza server e clicca \"Salva\".",
"step5": "Clicca \"Prova sincronizzazione\" per verificare la connessione.",
"step6": "Dopo aver completato questi passaggi, clicca {{- link}}.",
"step6-here": "qui"
},
"setup_sync-from-server": {
"heading": "Sincronizza dal server",
"instructions": "Inserire l'indirizzo e le credenziali del server Trilium. L'intero documento Trilium verrà scaricato dal server, e sarà configurata la sincronizzazione allo stesso. L'operazione potrebbe impiegare un po' di tempo, in base alla velocità della connessione e alla dimensione del documento.",
"server-host": "Indirizzo del server Trilium",
"server-host-placeholder": "https://<nome host>:<porta>",
"proxy-server": "Server proxy (facoltativo)",
"proxy-server-placeholder": "https://<nome host>:<porta>",
"note": "Note:",
"proxy-instruction": "Se il campo del proxy viene lasciato vuoto, il proxy di sistema verrà utilizzato (si applica solo all'applicazione desktop)",
"password": "Password",
"password-placeholder": "Password",
"back": "Indietro",
"finish-setup": "Termina configurazione"
},
"setup_sync-in-progress": {
"heading": "Sincronizzazione in corso",
"successful": "La sincronizzazione è stata configurata correttamente. Ci potrebbe volere un po' di tempo prima che la sincronizzazione iniziale termini. Appena sarà terminata, sarai ri-indirizzato alla pagina di accesso.",
"outstanding-items": "Elementi eccezionali in sincronizzazione:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Pagina non trovata",
"heading": "Pagina non trovata"

View File

@@ -220,47 +220,6 @@
"button": "パスワードの設定",
"password-confirmation": "パスワードの再入力"
},
"setup": {
"heading": "Trilium Notes セットアップ",
"new-document": "私は新しいユーザーで、ートを取るために新しいTriliumドキュメントを作成したい",
"sync-from-desktop": "すでにデスクトップ版のインスタンスがあり、同期を設定したい",
"sync-from-server": "すでにサーバー版のインスタンスがあり、同期を設定したい",
"init-in-progress": "ドキュメントの初期化処理を実行中",
"redirecting": "まもなくアプリケーションにリダイレクトされます。",
"next": "次へ",
"title": "セットアップ"
},
"setup_sync-from-desktop": {
"heading": "デスクトップから同期",
"description": "このセットアップはデスクトップインスタンスから開始する必要があります:",
"step1": "Trilium Notes のデスクトップインスタンスを開きます。",
"step2": "Triliumメニューから、設定をクリックします。",
"step3": "同期をクリックします。",
"step4": "サーバーインスタンスアドレスを {{- host}} に変更し、保存をクリックします。",
"step5": "「同期テスト」をクリックして、接続が成功したか確認してください。",
"step6": "これらのステップを完了したら、{{- link}} をクリックしてください。",
"step6-here": "ここ"
},
"setup_sync-from-server": {
"heading": "サーバーから同期",
"instructions": "Triliumサーバーのアドレスと認証情報を下記に入力してください。これにより、Triliumドキュメント全体がサーバーからダウンロードされ、同期が設定されます。ドキュメントのサイズと接続速度によっては、時間がかかる場合があります。",
"server-host": "Triliumサーバーのアドレス",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "プロキシサーバー(オプション)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"proxy-instruction": "プロキシ設定を空欄にすると、システムプロキシが使用されます(デスクトップアプリケーションにのみ適用されます)",
"password": "パスワード",
"password-placeholder": "パスワード",
"finish-setup": "セットアップ完了",
"back": "戻る",
"note": "ノート:"
},
"setup_sync-in-progress": {
"heading": "同期中",
"successful": "同期が正しく設定されました。最初の同期が完了するまでしばらく時間がかかります。完了すると、ログインページにリダイレクトされます。",
"outstanding-items": "同期が未完了のアイテム:",
"outstanding-items-default": "N/A"
},
"weekdays": {
"monday": "月曜日",
"tuesday": "火曜日",

View File

@@ -202,14 +202,5 @@
"password": "암호",
"password-confirmation": "암호 확인",
"button": "암호 설정"
},
"setup": {
"heading": "Trilium 노트 셋업",
"next": "다음",
"init-in-progress": "문서 초기화 진행 중",
"title": "셋업"
},
"setup_sync-from-desktop": {
"step5": "연결 설정이 성공적인지 확인을 위해 \"Test sync\" 버튼을 클릭하세요."
}
}

View File

@@ -18,10 +18,6 @@
"set_password": {
"password": "Passord"
},
"setup": {
"next": "Neste",
"title": "Konfigurasjon"
},
"login": {
"title": "Logg inn",
"password": "Passord",

View File

@@ -331,27 +331,6 @@
"password-confirmation": "Potwierdź hasło",
"button": "Ustaw hasło"
},
"setup": {
"heading": "Instalacja Trilium Notes",
"new-document": "Jestem nowym użytkownikiem i chcę stworzyć nową bazę danych dla moich notatek",
"sync-from-desktop": "Mam już aplikację desktopową i chcę skonfigurować z nią synchronizację",
"sync-from-server": "Mam już serwer i chcę skonfigurować z nim synchronizację",
"next": "Dalej",
"init-in-progress": "Trwa inicjalizacja dokumentu",
"redirecting": "Zaraz zostaniesz przekierowany do aplikacji.",
"title": "Instalacja"
},
"setup_sync-from-desktop": {
"heading": "Synchronizacja z aplikacji desktopowej",
"description": "Tę konfigurację należy rozpocząć w aplikacji desktopowej:",
"step1": "Otwórz aplikację desktopową Trilium Notes.",
"step2": "Z menu Trilium wybierz Opcje.",
"step3": "Przejdź do kategorii Synchronizacja.",
"step4": "Zmień adres serwera na: {{- host}} i kliknij Zapisz.",
"step5": "Kliknij przycisk \"Testuj synchronizację\", aby zweryfikować połączenie.",
"step6": "Po wykonaniu tych kroków kliknij {{- link}}.",
"step6-here": "tutaj"
},
"share_page": {
"no-content": "Ta notatka nie posiada treści.",
"parent": "nadrzędna:",

View File

@@ -220,47 +220,6 @@
"password-confirmation": "Confirmar Palavra-passe",
"button": "Definir palavra-passe"
},
"setup": {
"heading": "Trilium Notes setup",
"new-document": "Sou um novo utilizador e quero criar um documento Trilium para as minhas notas",
"sync-from-desktop": "Já tenho uma instância no desktop e quero configurar a sincronização com ela",
"sync-from-server": "Já tenho uma instância no servidor e quero configurar a sincronização com ela",
"next": "Avançar",
"init-in-progress": "Inicialização do documento em andamento",
"redirecting": "Será redirecionado para a aplicação brevemente.",
"title": "Configuração"
},
"setup_sync-from-desktop": {
"heading": "Sincronizar com Desktop",
"description": "Esta configuração deve ser iniciada a partir da instância do desktop:",
"step1": "Abra a sua instância do Trilium Notes no desktop.",
"step2": "No menu do Trilium, clique em Opções.",
"step3": "Clique na categoria Sincronização.",
"step4": "Altere o endereço da instância do servidor para: {{- host}} e clique em Gravar.",
"step5": "Clique no botão \"Testar sincronização\" para verificar se a conexão foi bem-sucedida.",
"step6": "Depois de concluir estas etapas, clique em {{- link}}.",
"step6-here": "aqui"
},
"setup_sync-from-server": {
"heading": "Sincronizar do Servidor",
"instructions": "Por favor, insira abaixo o endereço e as credenciais do servidor Trilium. Isto descarragará de todo o documento Trilium do servidor e configurará a sincronização com ele. Dependendo do tamanho do documento e da velocidade da conexão, isto pode levar algum tempo.",
"server-host": "Endereço do servidor Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Servidor proxy (opcional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Nota:",
"proxy-instruction": "Se deixar o campo de proxy em branco, o proxy do sistema será usado (aplicável apenas à aplicação desktop)",
"password": "Palavra-passe",
"password-placeholder": "Palavra-passe",
"back": "Voltar",
"finish-setup": "Terminar configuração"
},
"setup_sync-in-progress": {
"heading": "Sincronização em andamento",
"successful": "A sincronização foi configurada corretamente. Levará algum tempo para que a sincronização inicial seja concluída. Quando terminar, será redirecionado para a página de login.",
"outstanding-items": "Elementos de sincronização pendentes:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Não encontrado",
"heading": "Não encontrado"

View File

@@ -123,47 +123,6 @@
"password-confirmation": "Confirmar Senha",
"button": "Definir senha"
},
"setup": {
"heading": "Trilium Notes setup",
"new-document": "Sou um novo usuário e quero criar um novo documento Trilium para minhas notas",
"sync-from-desktop": "Já tenho uma instância no desktop e quero configurar a sincronização com ela",
"sync-from-server": "Já tenho uma instância no servidor e quero configurar a sincronização com ela",
"next": "Avançar",
"init-in-progress": "Inicialização do documento em andamento",
"redirecting": "Você será redirecionado para o aplicativo em breve.",
"title": "Setup"
},
"setup_sync-from-desktop": {
"heading": "Sincronizar com Desktop",
"description": "Esta configuração deve ser iniciada a partir da instância do desktop:",
"step1": "Abra sua instância do Trilium Notes no desktop.",
"step2": "No menu do Trilium, clique em Opções.",
"step3": "Clique na categoria Sincronização.",
"step4": "Altere o endereço da instância do servidor para: {{- host}} e clique em Salvar.",
"step5": "Clique no botão \"Testar sincronização\" para verificar se a conexão foi bem-sucedida.",
"step6": "Depois de concluir essas etapas, clique em {{- link}}.",
"step6-here": "Aqui"
},
"setup_sync-from-server": {
"heading": "Sincronizar do Servidor",
"instructions": "Por favor, insira abaixo o endereço e as credenciais do servidor Trilium. Isso fará o download de todo o documento Trilium do servidor e configurará a sincronização com ele. Dependendo do tamanho do documento e da velocidade da conexão, isso pode levar algum tempo.",
"server-host": "Endereço do servidor Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Servidor proxy (opcional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Nota:",
"proxy-instruction": "Se você deixar o campo de proxy em branco, o proxy do sistema será usado (aplicável apenas ao aplicativo desktop)",
"password": "Senha",
"password-placeholder": "Senha",
"back": "Voltar",
"finish-setup": "Terminar configuração"
},
"setup_sync-in-progress": {
"heading": "Sincronização em andamento",
"successful": "A sincronização foi configurada corretamente. Levará algum tempo para que a sincronização inicial seja concluída. Quando terminar, você será redirecionado para a página de login.",
"outstanding-items": "Itens de sincronização pendentes:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Não encontrado",
"heading": "Não encontrado"

View File

@@ -123,47 +123,6 @@
"password": "Parolă",
"password-confirmation": "Confirmarea parolei"
},
"setup": {
"heading": "Instalarea Trilium Notes",
"init-in-progress": "Se inițializează documentul",
"new-document": "Sunt un utilizator nou și doresc să creez un document Trilium pentru notițele mele",
"next": "Mai departe",
"redirecting": "În scurt timp veți fi redirecționat la aplicație.",
"sync-from-desktop": "Am deja o instanță de desktop și aș dori o sincronizare cu aceasta",
"sync-from-server": "Am deja o instanță de server și doresc o sincronizare cu aceasta",
"title": "Inițializare"
},
"setup_sync-from-desktop": {
"description": "Acești pași trebuie urmați de pe aplicația de desktop:",
"heading": "Sincronizare cu aplicația desktop",
"step1": "Deschideți aplicația Trilium Notes pentru desktop.",
"step2": "Din meniul Trilium, dați clic pe Opțiuni.",
"step3": "Clic pe categoria „Sincronizare”.",
"step4": "Schimbați adresa server-ului către: {{- host}} și apăsați „Salvează”.",
"step5": "Clic pe butonul „Testează sincronizarea” pentru a verifica dacă conexiunea a fost făcută cu succes.",
"step6": "După ce ați completat pașii, dați click {{- link}}.",
"step6-here": "aici"
},
"setup_sync-from-server": {
"back": "Înapoi",
"finish-setup": "Finalizează inițializarea",
"heading": "Sincronizare cu server-ul",
"instructions": "Introduceți adresa server-ului Trilium și credențialele în secțiunea de jos. Astfel se va descărca întregul document Trilium de pe server și se va configura sincronizarea cu acesta. În funcție de dimensiunea documentului și viteza rețelei, acest proces poate dura.",
"note": "De remarcat:",
"password": "Parolă",
"proxy-instruction": "Dacă lăsați câmpul de proxy nesetat, proxy-ul de sistem va fi folosit (valabil doar pentru aplicația de desktop)",
"proxy-server": "Server-ul proxy (opțional)",
"proxy-server-placeholder": "https://<sistem>:<port>",
"server-host": "Adresa server-ului Trilium",
"server-host-placeholder": "https://<sistem>:<port>",
"password-placeholder": "Parolă"
},
"setup_sync-in-progress": {
"heading": "Sincronizare în curs",
"outstanding-items": "Elemente de sincronizat:",
"outstanding-items-default": "-",
"successful": "Sincronizarea a fost configurată cu succes. Poate dura ceva timp pentru a finaliza sincronizarea inițială. După efectuarea ei se va redirecționa către pagina de autentificare."
},
"share_404": {
"heading": "Pagină negăsită",
"title": "Pagină negăsită"

View File

@@ -309,36 +309,6 @@
"button": "Установить пароль",
"description": "Прежде чем начать использовать Trilium через веб-браузер, вам необходимо установить пароль. Этот пароль будет использоваться для входа."
},
"setup": {
"next": "Далее",
"title": "Настройка",
"heading": "Настройка Trilium",
"new-document": "Я новый пользователь и хочу создать новый документ Trilium для своих заметок",
"sync-from-desktop": "У меня уже есть настольное приложение, и я хочу настроить синхронизацию с ним",
"sync-from-server": "У меня уже есть сервер, и я хочу настроить синхронизацию с ним",
"init-in-progress": "Идет инициализация документа",
"redirecting": "Вскоре вы будете перенаправлены на страницу приложения."
},
"setup_sync-from-server": {
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server-placeholder": "https://<hostname>:<port>",
"password": "Пароль",
"password-placeholder": "Пароль",
"back": "Назад",
"finish-setup": "Завершение настройки",
"heading": "Синхронизация с сервера",
"instructions": "Введите адрес сервера Trilium и учётные данные ниже. Документ Trilium будет полностью загружен с сервера и синхронизирован с ним. В зависимости от размера документа и скорости вашего соединения, это может занять некоторое время.",
"server-host": "Адрес сервера Trilium",
"proxy-server": "Прокси-сервер (необязательно)",
"note": "Заметка:",
"proxy-instruction": "Если оставить настройки прокси-сервера пустыми, будет использоваться системный прокси-сервер (применимо только к настольному приложению)"
},
"setup_sync-in-progress": {
"outstanding-items-default": "Н/Д",
"heading": "Синхронизация в процессе",
"successful": "Синхронизация настроена. Первоначальная синхронизация займёт некоторое время. После её завершения вы будете перенаправлены на страницу входа.",
"outstanding-items": "Оставшиеся элементы синхронизации:"
},
"special_notes": {
"search_prefix": "Поиск:"
},

View File

@@ -123,47 +123,6 @@
"password-confirmation": "確認密碼",
"button": "設定密碼"
},
"setup": {
"heading": "Trilium 筆記設定",
"new-document": "我是新用戶,我想為我的筆記建立一個新的 Trilium 文件",
"sync-from-desktop": "我已經擁有桌面版本,想設定與它進行同步",
"sync-from-server": "我已經擁有伺服器版本,想設定與它進行同步",
"next": "下一步",
"init-in-progress": "文件正在初始化",
"redirecting": "您即將被重新導向至應用程式。",
"title": "設定"
},
"setup_sync-from-desktop": {
"heading": "從桌面版同步",
"description": "此設定需要從桌面版本啟動:",
"step1": "打開您的桌面版 Trilium 筆記。",
"step2": "從 Trilium 選單中,點擊「選項」。",
"step3": "點擊「同步」類別。",
"step4": "將伺服器版網址更改為:{{- host}} 並點擊儲存。",
"step5": "點擊「測試同步」以驗證連接是否成功。",
"step6": "完成這些步驟後,點擊 {{- link}}。",
"step6-here": "這裡"
},
"setup_sync-from-server": {
"heading": "從伺服器同步",
"instructions": "請在下方輸入 Trilium 伺服器網址和密碼。這將從伺服器下載整個 Trilium 數據庫檔案並同步。取決於數據庫大小和您的連接速度,這可能需要一段時間。",
"server-host": "Trilium 伺服器網址",
"server-host-placeholder": "https://<主機名稱>:<端口>",
"proxy-server": "代理伺服器(可選)",
"proxy-server-placeholder": "https://<主機名稱>:<端口>",
"note": "注意:",
"proxy-instruction": "如果您將代理設定留空,將使用系統代理(僅適用於桌面版)",
"password": "密碼",
"password-placeholder": "密碼",
"back": "返回",
"finish-setup": "完成設定"
},
"setup_sync-in-progress": {
"heading": "正在同步",
"successful": "已正確設定同步。初次同步可能需要一些時間。完成後,您將被重新導向至登入頁面。",
"outstanding-items": "未完成的同步項目:",
"outstanding-items-default": "無"
},
"share_404": {
"title": "未找到",
"heading": "未找到"

View File

@@ -220,47 +220,6 @@
"password-confirmation": "Підтвердження пароля",
"button": "Встановити пароль"
},
"setup": {
"heading": "Налаштування Trilium Notes",
"new-document": "Я новий користувач і хочу створити новий документ Trilium для своїх нотаток",
"sync-from-desktop": "У мене вже є екземпляр для ПК і я хочу налаштувати синхронізацію з ним",
"sync-from-server": "У мене вже є екземпляр сервера, і я хочу налаштувати синхронізацію з ним",
"next": "Наступна",
"init-in-progress": "Триває ініціалізація документа",
"redirecting": "Невдовзі вас буде перенаправлено до програми.",
"title": "Налаштування"
},
"setup_sync-from-desktop": {
"heading": "Синхронізація з ПК",
"description": "Це налаштування потрібно ініціювати з екземпляра ПК:",
"step1": "Відкрийте свій екземпляр Trilium Notes на ПК.",
"step2": "У меню Trilium натисніть Параметри.",
"step3": "Натисніть на Категорія Синхронізації.",
"step4": "Змініть адресу екземпляра сервера на: {{- host}} та натисніть кнопку Зберегти.",
"step5": "Натисніть \"Тест синхронізації\", щоб перевірити успішність підключення.",
"step6": "Після виконання цих кроків натисніть {{- link}}.",
"step6-here": "тут"
},
"setup_sync-from-server": {
"heading": "Синхронізація з сервера",
"instructions": "Будь ласка, введіть адресу сервера Trilium та облікові дані нижче. Це завантажить весь документ Trilium із сервера та налаштує синхронізацію з ним. Залежно від розміру документа та швидкості вашого з’єднання, це може зайняти деякий час.",
"server-host": "Адреса сервера Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Проксі-сервер (необов'язково)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Нотатка:",
"proxy-instruction": "Якщо залишити налаштування проксі-сервера порожнім, використовуватиметься системний проксі-сервер (стосується лише програми на ПК)",
"password": "Пароль",
"password-placeholder": "Пароль",
"back": "Назад",
"finish-setup": "Завершити налаштування"
},
"setup_sync-in-progress": {
"heading": "Триває синхронізація",
"successful": "Синхронізацію налаштовано правильно. Початкова синхронізація завершиться через деякий час. Після її завершення вас буде перенаправлено на сторінку входу.",
"outstanding-items": "Незавершені елементи синхронізації:",
"outstanding-items-default": "Недоступно"
},
"share_404": {
"title": "Не знайдено",
"heading": "Не знайдено"

View File

@@ -1,178 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta id="syncInProgress" content="<%= syncInProgress ? 1 : 0 %>" />
<title><%= t("setup.title") %></title>
<style>
body {
/* Prevent the content from being rendered before the main stylesheet loads */
display: none;
}
.lds-ring {
display: inline-block;
position: relative;
width: 60px;
height: 60px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 48px;
height: 48px;
margin: 8px;
border: 6px solid black;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: black transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>">
<noscript><%= t("javascript-required") %></noscript>
<div class="container">
<div id="setup-dialog" class="col-md-12 col-lg-8 col-xl-6 mx-auto" style="padding-top: 25px; font-size: larger; display: none;">
<h1><%= t("setup.heading") %></h1>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<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">
<%= 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">
<%= 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">
<%= t("setup.sync-from-server") %>
</label>
</div>
<button type="submit" id="setup-type-next" class="btn btn-primary" disabled><%= t("setup.next") %></button>
</form>
</div>
<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;">
<div class="lds-ring" style="margin-right: 20px;">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div style="line-height: 60px;">
<p><%= t("setup.redirecting") %></p>
</div>
</div>
</div>
<div id="sync-from-desktop-section">
<h2><%= t("setup_sync-from-desktop.heading") %></h2>
<p><%= t("setup_sync-from-desktop.description") %></p>
<ol>
<li><%= t("setup_sync-from-desktop.step1") %></li>
<li><%= t("setup_sync-from-desktop.step2") %></li>
<li><%= t("setup_sync-from-desktop.step3") %></li>
<li><%- t("setup_sync-from-desktop.step4", { host: '<span id="current-host"></span>'}) %></li>
<li><%= t("setup_sync-from-desktop.step5") %></li>
<li><%- t("setup_sync-from-desktop.step6", { link: `<a href="/">${t("setup_sync-from-desktop.step6-here")}</a>` }) %></li>
</ol>
<button type="button" data-action="back" class="btn btn-secondary">Back</button>
</div>
<div id="sync-from-server-section">
<form id="sync-from-server-form">
<h2><%= t("setup_sync-from-server.heading") %></h2>
<p><%= t("setup_sync-from-server.instructions") %></p>
<div class="form-group">
<label for="sync-server-host"><%= t("setup_sync-from-server.server-host") %></label>
<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" 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" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
</div>
<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 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>
<div><%= t("setup_sync-in-progress.outstanding-items") %> <strong id="outstanding-syncs"><%= t("setup_sync-in-progress.outstanding-items-default") %></strong></div>
</div>
</div>
</div>
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
componentId: ''
};
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="<%= appPath %>/setup.js" crossorigin type="module"></script>
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet" />
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet" />
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
</body>
</html>

View File

@@ -3,12 +3,16 @@
* are loaded later and will result in an empty string.
*/
import { initializeCore } from "@triliumnext/core";
import { getLog,initializeCore, sql_init } from "@triliumnext/core";
import fs from "fs";
import { t } from "i18next";
import path from "path";
import ClsHookedExecutionContext from "./cls_provider.js";
import NodejsCryptoProvider from "./crypto_provider.js";
import ServerPlatformProvider from "./platform_provider.js";
import dataDirs from "./services/data_dir.js";
import port from "./services/port.js";
import NodeRequestProvider from "./services/request.js";
import WebSocketMessagingProvider from "./services/ws_messaging_provider.js";
import BetterSqlite3Provider from "./sql_provider.js";
@@ -44,12 +48,14 @@ async function startApplication() {
// the maxEntityChangeId has been incremented during failed transaction, need to recalculate
entity_changes.recalculateMaxEntityChangeId();
},
}
},
crypto: new NodejsCryptoProvider(),
request: new NodeRequestProvider(),
executionContext: new ClsHookedExecutionContext(),
messaging: new WebSocketMessagingProvider(),
schema: fs.readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
platform: new ServerPlatformProvider(),
translations: (await import("./services/i18n.js")).initializeTranslations,
extraAppInfo: {
nodeVersion: process.version,
@@ -58,6 +64,10 @@ async function startApplication() {
});
const startTriliumServer = (await import("./www.js")).default;
await startTriliumServer();
if (!sql_init.isDbInitialized()) {
getLog().banner(t("sql_init.db_not_initialized_server", { port }));
}
}
startApplication();

View File

@@ -0,0 +1,8 @@
import { getLog, PlatformProvider } from "@triliumnext/core";
export default class ServerPlatformProvider implements PlatformProvider {
crash(message: string): void {
getLog().error(message);
process.exit(1);
}
}

View File

@@ -1,6 +1,8 @@
import type { Request, Response } from "express";
import optionService from "../../services/options.js";
import type { OptionMap } from "@triliumnext/commons";
import { sql_init } from "@triliumnext/core";
import type { Request, Response } from "express";
import optionService from "../../services/options.js";
const SYSTEM_SANS_SERIF = [
"system-ui",
@@ -22,7 +24,7 @@ const SYSTEM_MONOSPACE = ["ui-monospace", "SFMono-Regular", "SF Mono", "Consolas
function getFontCss(req: Request, res: Response) {
res.setHeader("Content-Type", "text/css");
if (!optionService.getOptionBool("overrideThemeFonts")) {
if (!sql_init.isDbInitialized() || !optionService.getOptionBool("overrideThemeFonts")) {
res.send("");
return;

View File

@@ -1,5 +1,5 @@
import { BootstrapDefinition } from "@triliumnext/commons";
import { getSharedBootstrapItems, getSql, icon_packs as iconPackService } from "@triliumnext/core";
import { getSharedBootstrapItems, getSql, icon_packs as iconPackService, sql_init } from "@triliumnext/core";
import type { Request, Response } from "express";
import packageJson from "../../package.json" with { type: "json" };
@@ -16,8 +16,6 @@ import { generateCsrfToken } from "./csrf_protection.js";
type View = "desktop" | "mobile" | "print";
export function bootstrap(req: Request, res: Response) {
const options = optionService.getOptionMap();
// csrf-csrf v4 binds CSRF tokens to the session ID via HMAC. With saveUninitialized: false,
// a brand-new session is never persisted unless explicitly modified, so its cookie is never
// sent to the browser — meaning every request gets a different ephemeral session ID, and
@@ -27,6 +25,19 @@ export function bootstrap(req: Request, res: Response) {
req.session.csrfInitialized = true;
}
const isDbInitialized = sql_init.isDbInitialized();
const commonItems = getSharedBootstrapItems(assetPath, isDbInitialized);
if (!isDbInitialized) {
res.send({
...commonItems,
baseApiUrl: "api/",
componentId: ""
});
return;
}
const options = optionService.getOptionMap();
const csrfToken = generateCsrfToken(req, res, {
overwrite: false,
validateOnReuse: false // if validation fails, generate a new token instead of throwing an error
@@ -41,7 +52,8 @@ export function bootstrap(req: Request, res: Response) {
const sql = getSql();
res.send({
...getSharedBootstrapItems(assetPath),
...commonItems,
dbInitialized: true,
device: view,
csrfToken,
themeCssUrl: getThemeCssUrl(theme, themeNote),

View File

@@ -34,7 +34,6 @@ import passwordApiRoute from "./api/password.js";
import recoveryCodes from './api/recovery_codes.js';
import scriptRoute from "./api/script.js";
import senderRoute from "./api/sender.js";
import setupApiRoute from "./api/setup.js";
import systemInfoRoute from "./api/system_info.js";
import totp from './api/totp.js';
// API routes
@@ -83,11 +82,15 @@ function register(app: express.Application) {
routes.buildSharedApiRoutes({
route,
asyncRoute,
apiRoute,
asyncApiRoute,
apiResultHandler,
checkApiAuth: auth.checkApiAuth,
checkApiAuthOrElectron: auth.checkApiAuthOrElectron
checkApiAuthOrElectron: auth.checkApiAuthOrElectron,
checkAppNotInitialized: auth.checkAppNotInitialized,
checkCredentials: auth.checkCredentials,
loginRateLimiter
});
route(PUT, "/api/notes/:noteId/file", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateFile, apiResultHandler);
@@ -149,13 +152,6 @@ function register(app: express.Application) {
// docker health check
route(GET, "/api/health-check", [], () => ({ status: "ok" }), apiResultHandler);
// group of the services below are meant to be executed from the outside
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
asyncRoute(PST, "/api/setup/new-document", [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
asyncRoute(PST, "/api/setup/sync-from-server", [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
route(GET, "/api/setup/sync-seed", [loginRateLimiter, auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
asyncRoute(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
route(PST, "/api/login/sync", [loginRateLimiter], loginApiRoute.loginSync, apiResultHandler);
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
apiRoute(PST, "/api/login/protected", loginApiRoute.loginToProtectedSession);

View File

@@ -1,9 +1,11 @@
import sql from "../services/sql.js";
import type express from "express";
import session, { Store } from "express-session";
import sessionSecret from "../services/session_secret.js";
import config from "../services/config.js";
import log from "../services/log.js";
import type express from "express";
import sessionSecret from "../services/session_secret.js";
import sql from "../services/sql.js";
import sqlInit from "../services/sql_init.js";
/**
* The amount of time in milliseconds after which expired sessions are cleaned up.
@@ -20,6 +22,10 @@ export const SESSION_COOKIE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours
export class SQLiteSessionStore extends Store {
get(sid: string, callback: (err: any, session?: session.SessionData | null) => void): void {
if (!sqlInit.isDbInitialized()) {
return callback(null, null);
}
try {
const data = sql.getValue<string>(/*sql*/`SELECT data FROM sessions WHERE id = ?`, sid);
let session = null;
@@ -34,6 +40,10 @@ export class SQLiteSessionStore extends Store {
}
set(id: string, session: session.SessionData, callback?: (err?: any) => void): void {
if (!sqlInit.isDbInitialized()) {
return callback?.();
}
try {
const expires = session.cookie?.expires
? new Date(session.cookie.expires).getTime()
@@ -53,6 +63,10 @@ export class SQLiteSessionStore extends Store {
}
destroy(sid: string, callback?: (err?: any) => void): void {
if (!sqlInit.isDbInitialized()) {
return callback?.();
}
try {
sql.execute(/*sql*/`DELETE FROM sessions WHERE id = ?`, sid);
callback?.();
@@ -63,6 +77,10 @@ export class SQLiteSessionStore extends Store {
}
touch(sid: string, session: session.SessionData, callback?: (err?: any) => void): void {
if (!sqlInit.isDbInitialized()) {
return callback?.();
}
// For now it's only for session cookies ("Remember me" unchecked).
if (session.cookie?.expires) {
callback?.();
@@ -115,6 +133,8 @@ const sessionParser: express.RequestHandler = session({
export function startSessionCleanup() {
setInterval(() => {
if (!sqlInit.isDbInitialized()) return;
// Clean up expired sessions.
const now = Date.now();
const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now);

View File

@@ -1,12 +1,10 @@
"use strict";
import sqlInit from "../services/sql_init.js";
import setupService from "../services/setup.js";
import { isElectron } from "../services/utils.js";
import assetPath from "../services/asset_path.js";
import appPath from "../services/app_path.js";
import { i18n, setup as setupService } from "@triliumnext/core";
import type { Request, Response } from "express";
import { getCurrentLocale } from "../services/i18n.js";
import appPath from "../services/app_path.js";
import assetPath from "../services/asset_path.js";
import sqlInit from "../services/sql_init.js";
import { isElectron } from "../services/utils.js";
function setupPage(req: Request, res: Response) {
if (sqlInit.isDbInitialized()) {
@@ -29,10 +27,10 @@ function setupPage(req: Request, res: Response) {
}
res.render("setup", {
syncInProgress: syncInProgress,
assetPath: assetPath,
appPath: appPath,
currentLocale: getCurrentLocale()
syncInProgress,
assetPath,
appPath,
currentLocale: i18n.getCurrentLocale()
});
}

View File

@@ -16,7 +16,9 @@ refreshAuth();
function checkAuth(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
return res.redirect('setup');
// DB not initialized — let the request through so the client app
// can show its setup UI based on the bootstrap response.
return next();
}
const currentTotpStatus = totp.isTotpEnabled();
@@ -73,6 +75,10 @@ export function refreshAuth() {
// for electron things which need network stuff
// currently, we're doing that for file upload because handling form data seems to be difficult
function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
return next();
}
if (!req.session.loggedIn && !isElectron && !noAuthentication) {
console.warn(`Missing session with ID '${req.sessionID}'.`);
reject(req, res, "Logged in session not found");
@@ -82,6 +88,10 @@ function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction)
}
function checkApiAuth(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
return next();
}
if (!req.session.loggedIn && !noAuthentication) {
console.warn(`Missing session with ID '${req.sessionID}'.`);
reject(req, res, "Logged in session not found");
@@ -90,12 +100,9 @@ function checkApiAuth(req: Request, res: Response, next: NextFunction) {
}
}
function checkAppInitialized(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.redirect("setup");
} else {
next();
}
function checkAppInitialized(_req: Request, _res: Response, next: NextFunction) {
// Let the client app handle the uninitialized state via its setup UI.
next();
}
function checkPasswordSet(req: Request, res: Response, next: NextFunction) {

View File

@@ -1,138 +0,0 @@
import backupService from "./backup.js";
import sql from "./sql.js";
import log from "./log.js";
import { crash } from "./utils.js";
import appInfo from "./app_info.js";
import cls from "./cls.js";
import { t } from "i18next";
import MIGRATIONS from "../migrations/migrations.js";
interface MigrationInfo {
dbVersion: number;
/**
* If string, then the migration is an SQL script that will be executed.
* If a function, then the migration is a JavaScript/TypeScript module that will be executed.
*/
migration: string | (() => void);
}
async function migrate() {
const currentDbVersion = getDbVersion();
if (currentDbVersion < 214) {
await crash(t("migration.old_version"));
return;
}
// backup before attempting migration
if (!process.env.TRILIUM_INTEGRATION_TEST) {
await backupService.backupNow(
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
currentDbVersion === 214 ? `before-migration-v060` : "before-migration"
);
}
const migrations = await prepareMigrations(currentDbVersion);
// all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version
// otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app,
// and too old for the new app version.
cls.setMigrationRunning(true);
sql.transactional(() => {
for (const mig of migrations) {
try {
log.info(`Attempting migration to version ${mig.dbVersion}`);
executeMigration(mig);
sql.execute(
/*sql*/`UPDATE options
SET value = ?
WHERE name = ?`,
[mig.dbVersion.toString(), "dbVersion"]
);
log.info(`Migration to version ${mig.dbVersion} has been successful.`);
} catch (e: any) {
console.error(e);
crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack }));
break; // crash() is sometimes async
}
}
});
if (currentDbVersion === 214) {
// special VACUUM after the big migration
log.info("VACUUMing database, this might take a while ...");
sql.execute("VACUUM");
}
}
async function prepareMigrations(currentDbVersion: number): Promise<MigrationInfo[]> {
MIGRATIONS.sort((a, b) => a.version - b.version);
const migrations: MigrationInfo[] = [];
for (const migration of MIGRATIONS) {
const dbVersion = migration.version;
if (dbVersion > currentDbVersion) {
if ("sql" in migration) {
migrations.push({
dbVersion,
migration: migration.sql
});
} else {
// Due to ESM imports, the migration file needs to be imported asynchronously and thus cannot be loaded at migration time (since migration is not asynchronous).
// As such we have to preload the ESM.
migrations.push({
dbVersion,
migration: (await migration.module()).default
});
}
}
}
return migrations;
}
function executeMigration({ migration }: MigrationInfo) {
if (typeof migration === "string") {
console.log(`Migration with SQL script: ${migration}`);
sql.executeScript(migration);
} else {
console.log("Migration with JS module");
migration();
};
}
function getDbVersion() {
return parseInt(sql.getValue("SELECT value FROM options WHERE name = 'dbVersion'"));
}
function isDbUpToDate() {
const dbVersion = getDbVersion();
const upToDate = dbVersion >= appInfo.dbVersion;
if (!upToDate) {
log.info(`App db version is ${appInfo.dbVersion}, while db version is ${dbVersion}. Migration needed.`);
}
return upToDate;
}
async function migrateIfNecessary() {
const currentDbVersion = getDbVersion();
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== "true") {
await crash(t("migration.wrong_db_version", { version: currentDbVersion, targetVersion: appInfo.dbVersion }));
}
if (!isDbUpToDate()) {
await migrate();
}
}
export default {
migrateIfNecessary,
isDbUpToDate
};

View File

@@ -1,120 +0,0 @@
import syncService from "./sync.js";
import log from "./log.js";
import sqlInit from "./sql_init.js";
import optionService from "./options.js";
import syncOptions from "./sync_options.js";
import { request } from "@triliumnext/core";
import appInfo from "./app_info.js";
import { timeLimit } from "./utils.js";
import becca from "../becca/becca.js";
import type { SetupStatusResponse, SetupSyncSeedResponse } from "./api-interface.js";
async function hasSyncServerSchemaAndSeed() {
const response = await requestToSyncServer<SetupStatusResponse>("GET", "/api/setup/status");
if (response.syncVersion !== appInfo.syncVersion) {
throw new Error(
`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`
);
}
return response.schemaExists;
}
function triggerSync() {
log.info("Triggering sync.");
// it's ok to not wait for it here
syncService.sync().then((res) => {
if (res.success) {
sqlInit.setDbAsInitialized();
}
});
}
async function sendSeedToSyncServer() {
log.info("Initiating sync to server");
await requestToSyncServer<void>("POST", "/api/setup/sync-seed", {
options: getSyncSeedOptions(),
syncVersion: appInfo.syncVersion
});
// this is a completely new sync, need to reset counters. If this was not a new sync,
// the previous request would have failed.
optionService.setOption("lastSyncedPush", 0);
optionService.setOption("lastSyncedPull", 0);
}
async function requestToSyncServer<T>(method: string, path: string, body?: string | {}): Promise<T> {
const timeout = syncOptions.getSyncTimeout();
return (await timeLimit(
request.exec({
method,
url: syncOptions.getSyncServerHost() + path,
body,
proxy: syncOptions.getSyncProxy(),
timeout: timeout
}),
timeout
)) as T;
}
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
if (sqlInit.isDbInitialized()) {
return {
result: "failure",
error: "DB is already initialized."
};
}
try {
log.info("Getting document options FROM sync server.");
// the response is expected to contain documentId and documentSecret options
const resp = await request.exec<SetupSyncSeedResponse>({
method: "get",
url: `${syncServerHost}/api/setup/sync-seed`,
auth: { password },
proxy: syncProxy,
timeout: 30000 // seed request should not take long
});
if (resp.syncVersion !== appInfo.syncVersion) {
const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${resp.syncVersion}. To fix this issue, use same Trilium version on all instances.`;
log.error(message);
return {
result: "failure",
error: message
};
}
await sqlInit.createDatabaseForSync(resp.options, syncServerHost, syncProxy);
triggerSync();
return { result: "success" };
} catch (e: any) {
log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
return {
result: "failure",
error: e.message
};
}
}
function getSyncSeedOptions() {
return [becca.getOption("documentId"), becca.getOption("documentSecret")];
}
export default {
hasSyncServerSchemaAndSeed,
triggerSync,
sendSeedToSyncServer,
setupSyncFromSyncServer,
getSyncSeedOptions
};

View File

@@ -1,241 +1,14 @@
import { deferred, type OptionRow } from "@triliumnext/commons";
import { events as eventService } from "@triliumnext/core";
import fs from "fs";
import { t } from "i18next";
import { type OptionRow } from "@triliumnext/commons";
import { sql_init as coreSqlInit } from "@triliumnext/core";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import BOption from "../becca/entities/boption.js";
import backup from "./backup.js";
import cls from "./cls.js";
import config from "./config.js";
import password from "./encryption/password.js";
import hidden_subtree from "./hidden_subtree.js";
import zipImportService from "./import/zip.js";
import log from "./log.js";
import migrationService from "./migration.js";
import optionService from "./options.js";
import port from "./port.js";
import resourceDir from "./resource_dir.js";
import sql from "./sql.js";
import TaskContext from "./task_context.js";
import { isElectron } from "./utils.js";
export const dbReady = deferred<void>();
function schemaExists() {
return !!sql.getValue(/*sql*/`SELECT name FROM sqlite_master
WHERE type = 'table' AND name = 'options'`);
}
function isDbInitialized() {
if (!schemaExists()) {
return false;
}
const initialized = sql.getValue("SELECT value FROM options WHERE name = 'initialized'");
return initialized === "true";
}
async function initDbConnection() {
if (!isDbInitialized()) {
if (isElectron) {
log.info(t("sql_init.db_not_initialized_desktop"));
} else {
log.info(t("sql_init.db_not_initialized_server", { port }));
}
return;
}
await migrationService.migrateIfNecessary();
sql.execute('CREATE TEMP TABLE IF NOT EXISTS "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)');
sql.execute(`
CREATE TABLE IF NOT EXISTS "user_data"
(
tmpID INT,
username TEXT,
email TEXT,
userIDEncryptedDataKey TEXT,
userIDVerificationHash TEXT,
salt TEXT,
derivedKey TEXT,
isSetup TEXT DEFAULT "false",
UNIQUE (tmpID),
PRIMARY KEY (tmpID)
);`);
dbReady.resolve();
}
/**
* Applies the database schema, creating the necessary tables and importing the demo content.
*
* @param skipDemoDb if set to `true`, then the demo database will not be imported, resulting in an empty root note.
* @throws {Error} if the database is already initialized.
*/
async function createInitialDatabase(skipDemoDb?: boolean) {
if (isDbInitialized()) {
throw new Error("DB is already initialized");
}
const schema = fs.readFileSync(`${resourceDir.DB_INIT_DIR}/schema.sql`, "utf-8");
const demoFile = (!skipDemoDb ? fs.readFileSync(`${resourceDir.DB_INIT_DIR}/demo.zip`) : null);
let rootNote!: BNote;
// We have to import async since options init requires keyboard actions which require translations.
const optionsInitService = (await import("./options_init.js")).default;
const becca_loader = (await import("@triliumnext/core")).becca_loader;
sql.transactional(() => {
log.info("Creating database schema ...");
sql.executeScript(schema);
becca_loader.load();
log.info("Creating root note ...");
rootNote = new BNote({
noteId: "root",
title: "root",
type: "text",
mime: "text/html"
}).save();
rootNote.setContent("");
new BBranch({
noteId: "root",
parentNoteId: "none",
isExpanded: true,
notePosition: 10
}).save();
optionsInitService.initDocumentOptions();
optionsInitService.initNotSyncedOptions(true, {});
optionsInitService.initStartupOptions();
password.resetPassword();
});
// Check hidden subtree.
// This ensures the existence of system templates, for the demo content.
console.log("Checking hidden subtree at first start.");
cls.init(() => hidden_subtree.checkHiddenSubtree());
// Import demo content.
log.info("Importing demo content...");
const dummyTaskContext = new TaskContext("no-progress-reporting", "importNotes", null);
if (demoFile) {
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
}
// Post-demo.
sql.transactional(() => {
// this needs to happen after ZIP import,
// the previous solution was to move option initialization here, but then the important parts of initialization
// are not all in one transaction (because ZIP import is async and thus not transactional)
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
optionService.setOption(
"openNoteContexts",
JSON.stringify([
{
notePath: startNoteId,
active: true
}
])
);
});
log.info("Schema and initial content generated.");
initDbConnection();
}
async function createDatabaseForSync(options: OptionRow[], syncServerHost = "", syncProxy = "") {
log.info("Creating database for sync");
if (isDbInitialized()) {
throw new Error("DB is already initialized");
}
const schema = fs.readFileSync(`${resourceDir.DB_INIT_DIR}/schema.sql`, "utf8");
// We have to import async since options init requires keyboard actions which require translations.
const optionsInitService = (await import("./options_init.js")).default;
sql.transactional(() => {
sql.executeScript(schema);
optionsInitService.initNotSyncedOptions(false, { syncServerHost, syncProxy });
// document options required for sync to kick off
for (const opt of options) {
new BOption(opt).save();
}
});
log.info("Schema and not synced options generated.");
}
function setDbAsInitialized() {
if (!isDbInitialized()) {
optionService.setOption("initialized", "true");
initDbConnection();
// Emit an event to notify that the database is now initialized
eventService.emit(eventService.DB_INITIALIZED);
log.info("Database initialization completed, emitted DB_INITIALIZED event");
}
}
function optimize() {
if (config.General.readOnly) {
return;
}
log.info("Optimizing database");
const start = Date.now();
sql.execute("PRAGMA optimize");
log.info(`Optimization finished in ${Date.now() - start}ms.`);
}
export function getDbSize() {
return sql.getValue<number>("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()");
}
function initializeDb() {
cls.init(initDbConnection);
dbReady.then(() => {
if (config.General && config.General.noBackup === true) {
log.info("Disabling scheduled backups.");
return;
}
setInterval(() => backup.regularBackup(), 4 * 60 * 60 * 1000);
// kickoff first backup soon after start up
setTimeout(() => backup.regularBackup(), 5 * 60 * 1000);
// optimize is usually inexpensive no-op, so running it semi-frequently is not a big deal
setTimeout(() => optimize(), 60 * 60 * 1000);
setInterval(() => optimize(), 10 * 60 * 60 * 1000);
});
}
const schemaExists = coreSqlInit.schemaExists;
const isDbInitialized = coreSqlInit.isDbInitialized;
const dbReady = coreSqlInit.dbReady;
const setDbAsInitialized = coreSqlInit.setDbAsInitialized;
const createInitialDatabase = coreSqlInit.createInitialDatabase;
const initializeDb = coreSqlInit.initializeDb;
export const getDbSize = coreSqlInit.getDbSize;
const createDatabaseForSync = coreSqlInit.createDatabaseForSync;
export default {
dbReady,

View File

@@ -99,17 +99,6 @@ export function stripTags(text: string) {
return text.replace(/<(?:.|\n)*?>/gm, "");
}
export async function crash(message: string) {
if (isElectron) {
const electron = await import("electron");
electron.dialog.showErrorBox(t("modals.error_title"), message);
electron.app.exit(1);
} else {
log.error(message);
process.exit(1);
}
}
/** @deprecated */
export function getContentDisposition(filename: string) {
return coreUtils.getContentDisposition(filename);
@@ -405,7 +394,6 @@ export function waitForStreamToFinish(stream: any): Promise<void> {
export default {
compareVersions,
constantTimeCompare,
crash,
envToBoolean,
escapeHtml,
escapeRegExp,

View File

@@ -390,16 +390,21 @@ async function createSetupWindow() {
setupWindow = new BrowserWindow({
width,
height,
useContentSize: true,
resizable: false,
autoHideMenuBar: true,
title: "Trilium Notes Setup",
icon: getIcon(),
// Background effects (Mica on Windows, vibrancy on macOS)
...(isWindows && { backgroundMaterial: "mica" as const }),
...(isMac && { transparent: true, visualEffectState: "active" as const, vibrancy: "under-window" as const, titleBarStyle: "hiddenInset" as const }),
webPreferences: {
// necessary for e.g. utils.isElectron()
nodeIntegration: true
nodeIntegration: true,
contextIsolation: false
}
});
setupWindow.setMenuBarVisibility(false);
setupWindow.removeMenu();
setupWindow.loadURL(`http://127.0.0.1:${port}`);
setupWindow.on("closed", () => (setupWindow = null));
}

View File

@@ -30,6 +30,7 @@
"chore:generate-openapi": "tsx ./scripts/generate-openapi.ts",
"chore:update-build-info": "tsx ./scripts/update-build-info.ts",
"chore:update-version": "tsx ./scripts/update-version.ts",
"chore:wipe-node-modules": "node --experimental-strip-types ./scripts/wipe-node-modules.mts",
"docs:build": "pnpm run --filter build-docs start",
"docs:preview": "pnpm http-server site -p 9000",
"edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo",

View File

@@ -312,11 +312,26 @@ export interface DefinitionObject {
inverseRelation?: string;
}
export interface BootstrapDefinition {
device: "mobile" | "desktop" | "print" | false;
csrfToken: string;
/**
* Subset of bootstrap items that are available both in the main client and in the setup page.
*/
export interface BootstrapCommonItems {
dbInitialized: boolean;
baseApiUrl: string;
assetPath: string;
themeCssUrl: string | false;
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
iconPackCss: string;
iconRegistry: IconRegistry;
}
/**
* Bootstrap items that the client needs to start up. These are sent by the server in the HTML and made available as `window.glob`.
*/
export type BootstrapDefinition = BootstrapCommonItems & ({
dbInitialized: true;
device: "mobile" | "desktop" | "print" | false;
csrfToken: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";
platform?: typeof process.platform | "web";
@@ -332,15 +347,13 @@ export interface BootstrapDefinition {
isMainWindow: boolean;
isProtectedSessionAvailable: boolean;
triliumVersion: string;
assetPath: string;
appPath: string;
baseApiUrl: string;
currentLocale: Locale;
isRtl: boolean;
iconPackCss: string;
iconRegistry: IconRegistry;
TRILIUM_SAFE_MODE: boolean;
}
} | {
dbInitialized: false;
});
/**
* Response for /api/setup/status.
@@ -357,3 +370,10 @@ export interface SetupSyncSeedResponse {
syncVersion: number;
options: OptionRow[];
}
export type SetupSyncFromServerResponse = {
result: "success";
} | {
result: "failure";
error: string;
}

View File

@@ -10,7 +10,7 @@
"@braintree/sanitize-url": "7.1.1",
"@triliumnext/commons": "workspace:*",
"escape-html": "1.0.3",
"i18next": "25.7.3",
"i18next": "25.10.3",
"mime-types": "3.0.2",
"sanitize-filename": "1.6.4",
"sanitize-html": "2.17.2",

View File

@@ -17,14 +17,12 @@ import { getContext } from "../services/context.js";
export const beccaLoaded = new Promise<void>(async (res, rej) => {
// We have to import async since options init requires keyboard actions which require translations.
const options_init = (await import("../services/options_init.js")).default;
const { initStartupOptions } = await import("../services/options_init.js");
dbReady.then(() => {
getContext().init(() => {
load();
options_init.initStartupOptions();
getSql().transactional(() => initStartupOptions());
res();
});
});
@@ -290,6 +288,8 @@ eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
eventService.subscribeBeccaLoader(eventService.LEAVE_PROTECTED_SESSION, load);
export { load, reload };
export default {
load,
reload,

View File

@@ -6,10 +6,14 @@ import { SqlService, SqlServiceParams } from "./services/sql/sql";
import { initMessaging, MessagingProvider } from "./services/messaging/index";
import { initRequest, RequestProvider } from "./services/request";
import { initTranslations, TranslationProvider } from "./services/i18n";
import { initSchema } from "./services/sql_init";
import appInfo from "./services/app_info";
import PlatformProvider, { initPlatform } from "./services/platform";
export { getLog } from "./services/log";
export type * from "./services/sql/types";
export * from "./services/sql/index";
export { default as sql_init } from "./services/sql_init";
export * as protected_session from "./services/protected_session";
export { default as data_encryption } from "./services/encryption/data_encryption"
export * as binary_utils from "./services/utils/binary";
@@ -19,7 +23,7 @@ export { default as date_utils } from "./services/utils/date";
export { default as events } from "./services/events";
export { default as blob } from "./services/blob";
export { default as options } from "./services/options";
export { default as options_init } from "./services/options_init";
export * as options_init from "./services/options_init";
export { default as app_info } from "./services/app_info";
export { default as keyboard_actions } from "./services/keyboard_actions";
export { default as entity_changes } from "./services/entity_changes";
@@ -86,13 +90,18 @@ export { default as sync } from "./services/sync";
export { default as consistency_checks } from "./services/consistency_checks";
export { default as content_hash } from "./services/content_hash";
export { default as sync_mutex } from "./services/sync_mutex";
export { default as setup } from "./services/setup";
export { getPlatform, type PlatformProvider } from "./services/platform";
export { t } from "i18next";
export type { RequestProvider, ExecOpts, CookieJar } from "./services/request";
export async function initializeCore({ dbConfig, executionContext, crypto, translations, messaging, request, extraAppInfo }: {
export async function initializeCore({ dbConfig, executionContext, crypto, translations, messaging, request, schema, extraAppInfo, platform }: {
dbConfig: SqlServiceParams,
executionContext: ExecutionContext,
crypto: CryptoProvider,
translations: TranslationProvider,
platform: PlatformProvider,
schema: string,
messaging?: MessagingProvider,
request?: RequestProvider,
extraAppInfo?: {
@@ -100,11 +109,13 @@ export async function initializeCore({ dbConfig, executionContext, crypto, trans
dataDirectory: string;
};
}) {
initPlatform(platform);
initLog();
await initTranslations(translations);
initCrypto(crypto);
initSql(new SqlService(dbConfig, getLog()));
initContext(executionContext);
initSql(new SqlService(dbConfig, getLog()), dbConfig.onDatabaseNotInitialized);
initSchema(schema);
Object.assign(appInfo, extraAppInfo);
if (messaging) {
initMessaging(messaging);

View File

@@ -1,5 +1,5 @@
import sql from "../services/sql.js";
import utils from "../services/utils.js";
import { getSql } from "../services/sql/index";
import { hashedBlobId } from "../services/utils/index";
interface NoteContentsRow {
noteId: string;
@@ -17,9 +17,10 @@ interface NoteRevisionContents {
export default () => {
const existingBlobIds = new Set();
const sql = getSql();
for (const noteId of sql.getColumn<string>(/*sql*/`SELECT noteId FROM note_contents`)) {
const row = sql.getRow<NoteContentsRow>(/*sql*/`SELECT noteId, content, dateModified, utcDateModified FROM note_contents WHERE noteId = ?`, [noteId]);
const blobId = utils.hashedBlobId(row.content);
const blobId = hashedBlobId(row.content);
if (!existingBlobIds.has(blobId)) {
existingBlobIds.add(blobId);
@@ -42,7 +43,7 @@ export default () => {
for (const noteRevisionId of sql.getColumn(/*sql*/`SELECT noteRevisionId FROM note_revision_contents`)) {
const row = sql.getRow<NoteRevisionContents>(/*sql*/`SELECT noteRevisionId, content, utcDateModified FROM note_revision_contents WHERE noteRevisionId = ?`, [noteRevisionId]);
const blobId = utils.hashedBlobId(row.content);
const blobId = hashedBlobId(row.content);
if (!existingBlobIds.has(blobId)) {
existingBlobIds.add(blobId);

View File

@@ -1,12 +1,14 @@
import { becca_loader } from "@triliumnext/core";
import becca from "../becca/becca.js";
import cls from "../services/cls.js";
import log from "../services/log.js";
import sql from "../services/sql.js";
import { getLog } from "../services/log.js";
import { getSql } from "../services/sql/index.js";
import { getContext } from "../services/context.js";
import becca_loader from "../becca/becca_loader.js";
export default () => {
cls.init(() => {
getContext().init(() => {
const sql = getSql();
const log = getLog();
// emergency disabling of image compression since it appears to make problems in migration to 0.61
sql.execute(/*sql*/`UPDATE options SET value = 'false' WHERE name = 'compressImages'`);

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, beforeEach } from "vitest";
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import * as cls from "../services/context.js";
import { getSql } from "../services/sql/index.js";
import becca from "../becca/becca.js";
import becca_loader from "../becca/becca_loader.js";
import migration from "./0233__migrate_geo_map_to_collection.js";
@@ -19,12 +19,14 @@ import migration from "./0233__migrate_geo_map_to_collection.js";
* test data into the database, then verifies the migration transforms the data correctly.
*/
describe("Migration 0233: Migrate geoMap to collection", () => {
const sql = getSql();
beforeEach(async () => {
// Set up a clean in-memory database for each test
sql.rebuildIntegrationTestDatabase();
await new Promise<void>((resolve) => {
cls.init(() => {
cls.getContext().init(() => {
becca_loader.load();
resolve();
});
@@ -33,7 +35,7 @@ describe("Migration 0233: Migrate geoMap to collection", () => {
it("should migrate geoMap notes to book type with viewConfig attachment", async () => {
await new Promise<void>((resolve) => {
cls.init(() => {
cls.getContext().init(() => {
// Create a test geoMap note with content
const geoMapContent = JSON.stringify({
markers: [
@@ -164,7 +166,7 @@ describe("Migration 0233: Migrate geoMap to collection", () => {
it("should handle existing viewConfig attachments with same title", async () => {
await new Promise<void>((resolve) => {
cls.init(() => {
cls.getContext().init(() => {
const geoMapContent = JSON.stringify({ test: "data" });
const testNoteId = "test_geo_note_existing";
const testBlobId = "test_blob_geo_existing";
@@ -226,7 +228,7 @@ describe("Migration 0233: Migrate geoMap to collection", () => {
it("should handle protected geoMap notes appropriately", async () => {
await new Promise<void>((resolve, reject) => {
cls.init(() => {
cls.getContext().init(() => {
const geoMapContent = JSON.stringify({
markers: [{ lat: 51.5074, lng: -0.1278, title: "London" }],
center: { lat: 51.5074, lng: -0.1278 },

View File

@@ -1,11 +1,10 @@
import { becca_loader } from "@triliumnext/core";
import becca from "../becca/becca";
import cls from "../services/cls.js";
import becca_loader from "../becca/becca_loader";
import { getContext } from "../services/context";
import hidden_subtree from "../services/hidden_subtree";
export default () => {
cls.init(() => {
getContext().init(() => {
becca_loader.load();
// Ensure the geomap template is generated.

View File

@@ -1,9 +1,9 @@
import becca from "../becca/becca";
import becca_loader from "../becca/becca_loader";
import cls from "../services/cls.js";
import { getContext } from "../services/context";
export default () => {
cls.init(() => {
getContext().init(() => {
becca_loader.load();
for (const note of Object.values(becca.notes)) {

View File

@@ -1,10 +1,9 @@
"use strict";
import sqlInit from "../../services/sql_init.js";
import setupService from "../../services/setup.js";
import log from "../../services/log.js";
import { getLog } from "../../services/log.js";
import appInfo from "../../services/app_info.js";
import type { Request } from "express";
import { SetupSyncFromServerResponse } from "@triliumnext/commons";
function getStatus() {
return {
@@ -14,11 +13,12 @@ function getStatus() {
};
}
async function setupNewDocument() {
await sqlInit.createInitialDatabase();
async function setupNewDocument(req: Request) {
const { skipDemoDb } = req.query;
await sqlInit.createInitialDatabase(!!skipDemoDb);
}
function setupSyncFromServer(req: Request) {
function setupSyncFromServer(req: Request): Promise<SetupSyncFromServerResponse> {
const { syncServerHost, syncProxy, password } = req.body;
return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password);
@@ -27,6 +27,7 @@ function setupSyncFromServer(req: Request) {
function saveSyncSeed(req: Request) {
const { options, syncVersion } = req.body;
const log = getLog();
if (appInfo.syncVersion !== syncVersion) {
const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${syncVersion}. To fix this issue, use same Trilium version on all instances.`;
@@ -74,7 +75,7 @@ function saveSyncSeed(req: Request) {
* - user-password: []
*/
function getSyncSeed() {
log.info("Serving sync seed.");
getLog().info("Serving sync seed.");
return {
options: setupService.getSyncSeedOptions(),

View File

@@ -46,7 +46,8 @@ function getStats() {
const stats = {
initialized: getSql().getValue("SELECT value FROM options WHERE name = 'initialized'") === "true",
outstandingPullCount: syncService.getOutstandingPullCount()
outstandingPullCount: syncService.getOutstandingPullCount(),
totalPullCount: syncService.getTotalPullCount()
};
getLog().info(`Returning sync stats: ${JSON.stringify(stats)}`);

View File

@@ -23,6 +23,7 @@ import syncApiRoute from "./api/sync";
import autocompleteApiRoute from "./api/autocomplete";
import similarNotesRoute from "./api/similar_notes";
import imageRoute from "./api/image";
import setupApiRoute from "./api/setup";
// TODO: Deduplicate with routes.ts
const GET = "get",
@@ -33,14 +34,18 @@ const GET = "get",
interface SharedApiRoutesContext {
route: any;
asyncRoute: any;
apiRoute: any;
asyncApiRoute: any;
checkApiAuth: any;
apiResultHandler: any;
checkApiAuthOrElectron: any;
checkAppNotInitialized: any;
loginRateLimiter: any;
checkCredentials: any;
}
export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron }: SharedApiRoutesContext) {
export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron, checkAppNotInitialized, checkCredentials, loginRateLimiter }: SharedApiRoutesContext) {
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
@@ -111,6 +116,13 @@ export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiA
route(GET, "/api/attachments/:attachmentId/image/:filename", [checkApiAuthOrElectron], imageRoute.returnAttachedImage);
route(GET, "/api/images/:noteId/:filename", [checkApiAuthOrElectron], imageRoute.returnImageFromNote);
// group of the services below are meant to be executed from the outside
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
asyncRoute(PST, "/api/setup/new-document", [checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
asyncRoute(PST, "/api/setup/sync-from-server", [checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
route(GET, "/api/setup/sync-seed", [loginRateLimiter, checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
asyncRoute(PST, "/api/setup/sync-seed", [checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
asyncApiRoute(PST, "/api/sync/test", syncApiRoute.testSync);
asyncApiRoute(PST, "/api/sync/now", syncApiRoute.syncNow);
apiRoute(PST, "/api/sync/fill-entity-changes", syncApiRoute.fillEntityChanges);

View File

@@ -0,0 +1,5 @@
export default {
backupNow() {
console.warn("Backup not yet available.");
}
}

View File

@@ -5,13 +5,26 @@ import { generateCss, generateIconRegistry, getIconPacks, MIME_TO_EXTENSION_MAPP
import options from "./options";
import { getCurrentLocale } from "./i18n";
export default function getSharedBootstrapItems(assetPath: string): Pick<BootstrapDefinition, "assetPath" | "headingStyle" | "layoutOrientation" | "maxEntityChangeIdAtLoad" | "maxEntityChangeSyncIdAtLoad" | "isProtectedSessionAvailable" | "iconRegistry" | "iconPackCss" | "currentLocale" | "isRtl"> {
export default function getSharedBootstrapItems(assetPath: string, dbInitialized: boolean) {
const sql = getSql();
const iconPacks = getIconPacks();
const currentLocale = getCurrentLocale();
return {
const commonItems = {
assetPath,
dbInitialized,
...getIconConfig(assetPath)
};
if (!dbInitialized) {
return {
...commonItems,
themeCssUrl: false,
themeUseNextAsBase: "next"
};
}
return {
...commonItems,
headingStyle: options.getOption("headingStyle") as "plain" | "underline" | "markdown",
layoutOrientation: options.getOption("layoutOrientation") as "vertical" | "horizontal",
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
@@ -19,6 +32,13 @@ export default function getSharedBootstrapItems(assetPath: string): Pick<Bootstr
isProtectedSessionAvailable: protected_session.isProtectedSessionAvailable(),
currentLocale,
isRtl: !!currentLocale.rtl,
}
}
export function getIconConfig(assetPath: string): Pick<BootstrapDefinition, "iconRegistry" | "iconPackCss"> {
const iconPacks = getIconPacks();
return {
iconRegistry: generateIconRegistry(iconPacks),
iconPackCss: iconPacks
.map(p => generateCss(p, p.builtin
@@ -26,5 +46,5 @@ export default function getSharedBootstrapItems(assetPath: string): Pick<Bootstr
: `api/attachments/download/${p.fontAttachmentId}`))
.filter(Boolean)
.join("\n\n"),
}
};
}

View File

@@ -8,7 +8,7 @@ import BNote from "../becca/entities/bnote.js";
import buildLaunchBarConfig from "./hidden_subtree_launcherbar.js";
import buildHiddenSubtreeTemplates from "./hidden_subtree_templates.js";
import { cleanUpHelp, getHelpHiddenSubtreeData } from "./in_app_help.js";
import * as migrationService from "./migration.js";
import migrationService from "./migration.js";
import noteService from "./notes.js";
import { getLog } from "./log.js";

View File

@@ -28,14 +28,12 @@ function getCurrentLanguage(): LOCALE_IDS {
let language: string | null = null;
if (sql_init.isDbInitialized()) {
language = options.getOptionOrNull("locale");
if (!language) {
console.info("Language option not found, falling back to en.");
}
}
if (!language) {
console.info("Language option not found, falling back to en.");
language = "en";
}
return language as LOCALE_IDS;
return (language ?? "en") as LOCALE_IDS;
}
export async function changeLanguage(locale: string) {
@@ -44,6 +42,11 @@ export async function changeLanguage(locale: string) {
}
export function getCurrentLocale() {
if (!sql_init.isDbInitialized()) {
// If DB is not initialized, we cannot get the locale from options, so we return English as a default.
return LOCALES.find(l => l.id === "en")!;
}
const localeId = options.getOptionOrNull("locale") ?? "en";
const currentLocale = LOCALES.find(l => l.id === localeId);
if (!currentLocale) return LOCALES.find(l => l.id === "en")!;

View File

@@ -12,6 +12,30 @@ export default class LogService {
console.error("ERROR: ", message);
}
banner(message: string) {
const maxContent = 76; // 80 - 4 (border + padding)
const words = message.split(" ");
const lines: string[] = [];
let current = "";
for (const word of words) {
const candidate = current ? `${current} ${word}` : word;
if (candidate.length > maxContent) {
if (current) lines.push(current);
current = word;
} else {
current = candidate;
}
}
if (current) lines.push(current);
const width = Math.min(Math.max(...lines.map((l) => l.length)), maxContent) + 4;
const top = `${"═".repeat(width)}`;
const mid = lines.map((l) => `${l.padEnd(width - 4)}`).join("\n");
const bot = `${"═".repeat(width)}`;
console.log(`\n${top}\n${mid}\n${bot}\n`);
}
}
let log: LogService;

View File

@@ -1,4 +1,138 @@
export function isDbUpToDate() {
// TODO: Implement.
return true;
import backupService from "./backup.js";
import { getSql } from "./sql/index.js";
import { getLog } from "./log.js";
import { getPlatform } from "./platform.js";
import appInfo from "./app_info.js";
import * as cls from "./context.js";
import { t } from "i18next";
import MIGRATIONS from "../migrations/migrations.js";
interface MigrationInfo {
dbVersion: number;
/**
* If string, then the migration is an SQL script that will be executed.
* If a function, then the migration is a JavaScript/TypeScript module that will be executed.
*/
migration: string | (() => void);
}
async function migrate() {
const currentDbVersion = getDbVersion();
if (currentDbVersion < 214) {
getPlatform().crash(t("migration.old_version"));
}
// backup before attempting migration
if (!process.env.TRILIUM_INTEGRATION_TEST) {
await backupService.backupNow(
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
currentDbVersion === 214 ? `before-migration-v060` : "before-migration"
);
}
const migrations = await prepareMigrations(currentDbVersion);
// all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version
// otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app,
// and too old for the new app version.
cls.setMigrationRunning(true);
const sql = getSql();
const log = getLog();
sql.transactional(() => {
for (const mig of migrations) {
try {
log.info(`Attempting migration to version ${mig.dbVersion}`);
executeMigration(mig);
sql.execute(
/*sql*/`UPDATE options
SET value = ?
WHERE name = ?`,
[mig.dbVersion.toString(), "dbVersion"]
);
log.info(`Migration to version ${mig.dbVersion} has been successful.`);
} catch (e: any) {
console.error(e);
getPlatform().crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack }));
}
}
});
if (currentDbVersion === 214) {
// special VACUUM after the big migration
log.info("VACUUMing database, this might take a while ...");
sql.execute("VACUUM");
}
}
async function prepareMigrations(currentDbVersion: number): Promise<MigrationInfo[]> {
MIGRATIONS.sort((a, b) => a.version - b.version);
const migrations: MigrationInfo[] = [];
for (const migration of MIGRATIONS) {
const dbVersion = migration.version;
if (dbVersion > currentDbVersion) {
if ("sql" in migration) {
migrations.push({
dbVersion,
migration: migration.sql
});
} else {
// Due to ESM imports, the migration file needs to be imported asynchronously and thus cannot be loaded at migration time (since migration is not asynchronous).
// As such we have to preload the ESM.
migrations.push({
dbVersion,
migration: (await migration.module()).default
});
}
}
}
return migrations;
}
function executeMigration({ migration }: MigrationInfo) {
if (typeof migration === "string") {
console.log(`Migration with SQL script: ${migration}`);
getSql().executeScript(migration);
} else {
console.log("Migration with JS module");
migration();
};
}
function getDbVersion() {
return parseInt(getSql().getValue("SELECT value FROM options WHERE name = 'dbVersion'"));
}
function isDbUpToDate() {
const dbVersion = getDbVersion();
const upToDate = dbVersion >= appInfo.dbVersion;
if (!upToDate) {
getLog().info(`App db version is ${appInfo.dbVersion}, while db version is ${dbVersion}. Migration needed.`);
}
return upToDate;
}
async function migrateIfNecessary() {
const currentDbVersion = getDbVersion();
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== "true") {
getPlatform().crash(t("migration.wrong_db_version", { version: currentDbVersion, targetVersion: appInfo.dbVersion }));
}
if (!isDbUpToDate()) {
await migrate();
}
}
export default {
migrateIfNecessary,
isDbUpToDate
};

View File

@@ -7,7 +7,7 @@ import { getLog } from "./log.js";
import optionService from "./options.js";
import { isWindows, randomSecureToken } from "./utils/index.js";
function initDocumentOptions() {
export function initDocumentOptions() {
optionService.createOption("documentId", randomSecureToken(16), false);
optionService.createOption("documentSecret", randomSecureToken(16), false);
}
@@ -40,7 +40,7 @@ interface DefaultOption {
* @param initialized `true` if the database has been fully initialized (i.e. a new database was created), or `false` if the database is created for sync.
* @param opts additional options to be initialized, for example the sync configuration.
*/
async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = {}) {
export async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = {}) {
optionService.createOption(
"openNoteContexts",
JSON.stringify([
@@ -217,7 +217,7 @@ const defaultOptions: DefaultOption[] = [
*
* This method is called regardless of whether a new database is created, or an existing database is used.
*/
function initStartupOptions() {
export function initStartupOptions() {
const optionsMap = optionService.getOptionMap();
const allDefaultOptions = defaultOptions.concat(getKeyboardDefaultOptions());
@@ -258,8 +258,3 @@ function getKeyboardDefaultOptions() {
})) as DefaultOption[];
}
export default {
initDocumentOptions,
initNotSyncedOptions,
initStartupOptions
};

View File

@@ -0,0 +1,17 @@
/**
* Interface for platform-specific services. This is used to abstract away platform-specific implementations, such as crash reporting, from the core logic of the application.
*/
export interface PlatformProvider {
crash(message: string): void;
}
let platformProvider: PlatformProvider | null = null;
export function initPlatform(provider: PlatformProvider) {
platformProvider = provider;
}
export function getPlatform(): PlatformProvider {
if (!platformProvider) throw new Error("Platform provider not initialized");
return platformProvider;
}

View File

@@ -6,7 +6,7 @@ import syncOptions from "./sync_options.js";
import appInfo from "./app_info.js";
import { timeLimit } from "./utils/index.js";
import becca from "../becca/becca.js";
import type { SetupStatusResponse, SetupSyncSeedResponse } from "@triliumnext/commons";
import type { SetupStatusResponse, SetupSyncFromServerResponse, SetupSyncSeedResponse } from "@triliumnext/commons";
import request from "./request.js";
async function hasSyncServerSchemaAndSeed() {
@@ -61,7 +61,7 @@ async function requestToSyncServer<T>(method: string, path: string, body?: strin
)) as T;
}
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string): Promise<SetupSyncFromServerResponse> {
if (sqlInit.isDbInitialized()) {
return {
result: "failure",

View File

@@ -1,3 +1,4 @@
import sql_init from "../sql_init";
import type { SqlService } from "./sql";
let sql: SqlService | null = null;
@@ -5,6 +6,7 @@ let sql: SqlService | null = null;
export function initSql(instance: SqlService) {
if (sql) throw new Error("SQL already initialized");
sql = instance;
sql_init.initializeDb();
}
export function getSql(): SqlService {

View File

@@ -11,6 +11,7 @@ export interface SqlServiceParams {
provider: DatabaseProvider;
onTransactionRollback: () => void;
onTransactionCommit: () => void;
onDatabaseNotInitialized?: () => void;
isReadOnly: boolean;
}

View File

@@ -1,30 +1,238 @@
import { deferred } from "@triliumnext/commons";
import { deferred, OptionRow } from "@triliumnext/commons";
import { getSql } from "./sql";
import { getLog } from "./log";
import optionService from "./options";
import eventService from "./events";
import { getContext } from "./context";
import config from "./config";
import BNote from "../becca/entities/bnote";
import BBranch from "../becca/entities/bbranch";
import hidden_subtree from "./hidden_subtree";
import TaskContext from "./task_context";
import BOption from "../becca/entities/boption";
import migrationService from "./migration";
export const dbReady = deferred<void>();
// TODO: Proper impl.
setTimeout(() => {
dbReady.resolve();
}, 850);
let schema: string;
function isDbInitialized() {
return true;
}
async function createDatabaseForSync(a: any, b: string, c: any) {
console.error("createDatabaseForSync is not implemented yet");
}
function setDbAsInitialized() {
// Noop.
export function initSchema(schemaStr: string) {
schema = schemaStr;
}
function schemaExists() {
return true;
return !!getSql().getValue(/*sql*/`SELECT name FROM sqlite_master
WHERE type = 'table' AND name = 'options'`);
}
function isDbInitialized() {
try {
if (!schemaExists()) {
return false;
}
const initialized = getSql().getValue("SELECT value FROM options WHERE name = 'initialized'");
return initialized === "true";
} catch (e) {
return false;
}
}
async function initDbConnection() {
if (!isDbInitialized()) {
return;
}
await migrationService.migrateIfNecessary();
const sql = getSql();
sql.execute('CREATE TEMP TABLE IF NOT EXISTS "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)');
sql.execute(`
CREATE TABLE IF NOT EXISTS "user_data"
(
tmpID INT,
username TEXT,
email TEXT,
userIDEncryptedDataKey TEXT,
userIDVerificationHash TEXT,
salt TEXT,
derivedKey TEXT,
isSetup TEXT DEFAULT "false",
UNIQUE (tmpID),
PRIMARY KEY (tmpID)
);`);
dbReady.resolve();
}
function setDbAsInitialized() {
if (!isDbInitialized()) {
optionService.setOption("initialized", "true");
initDbConnection();
// Emit an event to notify that the database is now initialized
eventService.emit(eventService.DB_INITIALIZED);
getLog().info("Database initialization completed, emitted DB_INITIALIZED event");
}
}
function getDbSize() {
return 1000;
return getSql().getValue<number>("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()");
}
export default { isDbInitialized, createDatabaseForSync, setDbAsInitialized, schemaExists, getDbSize, dbReady };
function optimize() {
if (config.General.readOnly) {
return;
}
const log = getLog();
log.info("Optimizing database");
const start = Date.now();
getSql().execute("PRAGMA optimize");
log.info(`Optimization finished in ${Date.now() - start}ms.`);
}
function initializeDb() {
getContext().init(initDbConnection);
dbReady.then(() => {
// TODO: Re-enable backup.
// if (config.General && config.General.noBackup === true) {
// log.info("Disabling scheduled backups.");
// return;
// }
// setInterval(() => backup.regularBackup(), 4 * 60 * 60 * 1000);
// // kickoff first backup soon after start up
// setTimeout(() => backup.regularBackup(), 5 * 60 * 1000);
// // optimize is usually inexpensive no-op, so running it semi-frequently is not a big deal
// setTimeout(() => optimize(), 60 * 60 * 1000);
// setInterval(() => optimize(), 10 * 60 * 60 * 1000);
});
}
/**
* Applies the database schema, creating the necessary tables and importing the demo content.
*
* @param skipDemoDb if set to `true`, then the demo database will not be imported, resulting in an empty root note.
* @throws {Error} if the database is already initialized.
*/
async function createInitialDatabase(skipDemoDb?: boolean) {
if (isDbInitialized()) {
throw new Error("DB is already initialized");
}
let rootNote!: BNote;
// We have to import async since options init requires keyboard actions which require translations.
const { initDocumentOptions, initNotSyncedOptions, initStartupOptions } = await import("./options_init.js");
const { load: loadBecca } = await import("../becca/becca_loader.js");
const sql = getSql();
const log = getLog();
sql.transactional(() => {
log.info("Creating database schema ...");
sql.executeScript(schema);
loadBecca();
log.info("Creating root note ...");
rootNote = new BNote({
noteId: "root",
title: "root",
type: "text",
mime: "text/html"
}).save();
rootNote.setContent("");
new BBranch({
noteId: "root",
parentNoteId: "none",
isExpanded: true,
notePosition: 10
}).save();
// Bring in option init.
initDocumentOptions();
initNotSyncedOptions(true, {});
initStartupOptions();
// password.resetPassword();
});
// Check hidden subtree.
// This ensures the existence of system templates, for the demo content.
console.log("Checking hidden subtree at first start.");
getContext().init(() => {
getSql().transactional(() => hidden_subtree.checkHiddenSubtree());
});
// Import demo content.
log.info("Importing demo content...");
const dummyTaskContext = new TaskContext("no-progress-reporting", "importNotes", null);
// if (demoFile) {
// await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
// }
// Post-demo.
sql.transactional(() => {
// this needs to happen after ZIP import,
// the previous solution was to move option initialization here, but then the important parts of initialization
// are not all in one transaction (because ZIP import is async and thus not transactional)
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
optionService.setOption(
"openNoteContexts",
JSON.stringify([
{
notePath: startNoteId,
active: true
}
])
);
});
log.info("Schema and initial content generated.");
initDbConnection();
}
async function createDatabaseForSync(options: OptionRow[], syncServerHost = "", syncProxy = "") {
const log = getLog();
const sql = getSql();
log.info("Creating database for sync");
if (isDbInitialized()) {
throw new Error("DB is already initialized");
}
// We have to import async since options init requires keyboard actions which require translations.
const { initNotSyncedOptions } = await import("./options_init.js");
sql.transactional(() => {
sql.executeScript(schema);
initNotSyncedOptions(false, { syncServerHost, syncProxy });
// document options required for sync to kick off
for (const opt of options) {
new BOption(opt).save();
}
});
log.info("Schema and not synced options generated.");
}
export default { isDbInitialized, createDatabaseForSync, setDbAsInitialized, schemaExists, getDbSize, initDbConnection, dbReady, initializeDb, createInitialDatabase };

View File

@@ -26,6 +26,7 @@ import { getCrypto } from "./encryption/crypto.js";
let proxyToggle = true;
let outstandingPullCount = 0;
let totalPullCount: number | null = null;
interface CheckResponse {
maxEntityChangeId: number;
@@ -172,6 +173,10 @@ async function pullChanges(syncContext: SyncContext) {
outstandingPullCount = resp.outstandingPullCount;
if (totalPullCount === null) {
totalPullCount = entityChanges.length + outstandingPullCount;
}
const pulledDate = Date.now();
getSql().transactional(() => {
@@ -201,6 +206,8 @@ async function pullChanges(syncContext: SyncContext) {
}
log.info("Finished pull");
totalPullCount = null;
}
async function pushChanges(syncContext: SyncContext) {
@@ -450,6 +457,10 @@ function getOutstandingPullCount() {
return outstandingPullCount;
}
function getTotalPullCount() {
return totalPullCount;
}
function startSyncTimer() {
becca_loader.beccaLoaded.then(() => {
setInterval(cls.wrap(sync), 60000);
@@ -467,6 +478,7 @@ export default {
login,
getEntityChangeRecords,
getOutstandingPullCount,
getTotalPullCount,
getMaxEntityChangeId,
startSyncTimer
};

21
pnpm-lock.yaml generated
View File

@@ -680,6 +680,9 @@ importers:
'@triliumnext/commons':
specifier: workspace:*
version: link:../../packages/commons
'@triliumnext/core':
specifier: workspace:*
version: link:../../packages/trilium-core
'@triliumnext/server':
specifier: workspace:*
version: link:../server
@@ -1708,8 +1711,8 @@ importers:
specifier: 1.0.3
version: 1.0.3
i18next:
specifier: 25.7.3
version: 25.7.3(typescript@5.9.3)
specifier: 25.10.3
version: 25.10.3(typescript@5.9.3)
mime-types:
specifier: 3.0.2
version: 3.0.2
@@ -17224,6 +17227,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
'@ckeditor/ckeditor5-widget': 47.6.1
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-cloud-services@47.6.1':
dependencies:
@@ -17660,8 +17665,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.6.1
ckeditor5: 47.6.1
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-icons@47.6.1': {}
@@ -17679,8 +17682,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.6.1
ckeditor5: 47.6.1
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-import-word@47.6.1':
dependencies:
@@ -17704,8 +17705,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-inspector@5.0.0': {}
@@ -17716,8 +17715,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-line-height@47.6.1':
dependencies:
@@ -17812,8 +17809,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-merge-fields@47.6.1':
dependencies:
@@ -17834,6 +17829,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-operations-compressor@47.6.1':
dependencies:

View File

@@ -0,0 +1,24 @@
import { rm } from "fs/promises";
import { glob } from "glob";
import path from "path";
const root = path.resolve(import.meta.dirname, "..");
const dirs = await glob([
"node_modules",
"apps/*/node_modules",
"packages/*/node_modules"
], {
cwd: root,
absolute: true
});
if (dirs.length === 0) {
console.log("No node_modules directories found.");
} else {
for (const dir of dirs) {
console.log(`Removing ${path.relative(root, dir)}`);
await rm(dir, { recursive: true, force: true });
}
console.log(`Done. Removed ${dirs.length} node_modules director${dirs.length === 1 ? "y" : "ies"}.`);
}