mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 03:16:35 +02:00
Merge remote-tracking branch 'origin/standalone' into standalone-mobile-test
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { type BindableValue, default as sqlite3InitModule } from "@sqlite.org/sqlite-wasm";
|
||||
import { type BindableValue, type SAHPoolUtil, default as sqlite3InitModule } from "@sqlite.org/sqlite-wasm";
|
||||
import type { DatabaseProvider, RunResult, Statement, Transaction } from "@triliumnext/core";
|
||||
|
||||
// Type definitions for SQLite WASM (the library doesn't export these directly)
|
||||
@@ -227,6 +227,10 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
// OPFS state tracking
|
||||
private opfsDbPath?: string;
|
||||
|
||||
// SAHPool state tracking
|
||||
private sahPoolUtil?: SAHPoolUtil;
|
||||
private sahPoolDbName?: string;
|
||||
|
||||
/**
|
||||
* Get the SQLite WASM module version info.
|
||||
* Returns undefined if the module hasn't been initialized yet.
|
||||
@@ -287,16 +291,111 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
return this.sqlite3 !== undefined;
|
||||
}
|
||||
|
||||
// ==================== OPFS Support ====================
|
||||
// ==================== SAHPool VFS (preferred OPFS backend) ====================
|
||||
|
||||
/**
|
||||
* Check if the OPFS VFS is available.
|
||||
* Install the OPFS SAHPool VFS. This pre-allocates a pool of OPFS
|
||||
* SyncAccessHandle objects, enabling WAL mode and significantly faster
|
||||
* writes compared to the legacy OPFS VFS.
|
||||
*
|
||||
* Must be called after `initWasm()` and before `loadFromSahPool()`.
|
||||
* This is async because it acquires OPFS file handles.
|
||||
*
|
||||
* @param options.directory - OPFS directory for the pool (default: auto-derived from VFS name)
|
||||
* @param options.initialCapacity - Minimum number of file slots (default: 6)
|
||||
* @throws Error if the environment doesn't support SAHPool (no OPFS, no Worker, no COOP/COEP)
|
||||
*/
|
||||
async installSahPool(options: { directory?: string; initialCapacity?: number } = {}): Promise<void> {
|
||||
this.ensureSqlite3();
|
||||
|
||||
console.log("[BrowserSqlProvider] Installing OPFS SAHPool VFS...");
|
||||
const startTime = performance.now();
|
||||
|
||||
this.sahPoolUtil = await this.sqlite3!.installOpfsSAHPoolVfs({
|
||||
clearOnInit: false,
|
||||
initialCapacity: options.initialCapacity ?? 6,
|
||||
directory: options.directory,
|
||||
});
|
||||
|
||||
// Ensure enough slots for DB + WAL + journal + temp files
|
||||
await this.sahPoolUtil.reserveMinimumCapacity(options.initialCapacity ?? 6);
|
||||
|
||||
const initTime = performance.now() - startTime;
|
||||
console.log(
|
||||
`[BrowserSqlProvider] SAHPool VFS installed in ${initTime.toFixed(2)}ms ` +
|
||||
`(capacity: ${this.sahPoolUtil.getCapacity()}, files: ${this.sahPoolUtil.getFileCount()})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the SAHPool VFS has been successfully installed.
|
||||
*/
|
||||
get isSahPoolInstalled(): boolean {
|
||||
return this.sahPoolUtil !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the SAHPool utility for advanced operations (import/export/migration).
|
||||
*/
|
||||
get sahPool(): SAHPoolUtil | undefined {
|
||||
return this.sahPoolUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load or create a database using the SAHPool VFS.
|
||||
* This is the preferred method for persistent storage — it supports WAL mode
|
||||
* and is significantly faster than the legacy OPFS VFS.
|
||||
*
|
||||
* @param dbName - Virtual filename within the pool (e.g., "/trilium.db").
|
||||
* Must start with a slash.
|
||||
* @throws Error if SAHPool VFS is not installed
|
||||
*/
|
||||
loadFromSahPool(dbName: string): void {
|
||||
this.ensureSqlite3();
|
||||
if (!this.sahPoolUtil) {
|
||||
throw new Error(
|
||||
"SAHPool VFS not installed. Call installSahPool() first."
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[BrowserSqlProvider] Loading database from SAHPool: ${dbName}`);
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
this.db = new this.sahPoolUtil.OpfsSAHPoolDb(dbName);
|
||||
this.sahPoolDbName = dbName;
|
||||
this.opfsDbPath = undefined;
|
||||
|
||||
// SAHPool supports WAL mode — the key advantage over legacy OPFS VFS
|
||||
this.db.exec("PRAGMA journal_mode = WAL");
|
||||
this.db.exec("PRAGMA synchronous = NORMAL");
|
||||
|
||||
const loadTime = performance.now() - startTime;
|
||||
console.log(`[BrowserSqlProvider] SAHPool database loaded in ${loadTime.toFixed(2)}ms (WAL mode)`);
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
console.error(`[BrowserSqlProvider] Failed to load SAHPool database: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the currently open database is using the SAHPool VFS.
|
||||
*/
|
||||
get isUsingSahPool(): boolean {
|
||||
return this.sahPoolDbName !== undefined;
|
||||
}
|
||||
|
||||
// ==================== Legacy OPFS Support ====================
|
||||
|
||||
/**
|
||||
* Check if the legacy OPFS VFS is available.
|
||||
* This requires:
|
||||
* - Running in a Worker context
|
||||
* - Browser support for OPFS APIs
|
||||
* - COOP/COEP headers sent by the server (for SharedArrayBuffer)
|
||||
*
|
||||
* @returns true if OPFS VFS is available for use
|
||||
* @returns true if legacy OPFS VFS is available for use
|
||||
*/
|
||||
isOpfsAvailable(): boolean {
|
||||
this.ensureSqlite3();
|
||||
@@ -307,6 +406,9 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
|
||||
/**
|
||||
* Load or create a database stored in OPFS for persistent storage.
|
||||
*
|
||||
* **Prefer `loadFromSahPool()` over this method** — it supports WAL mode
|
||||
* and is significantly faster. This method is kept for migration purposes.
|
||||
* The database will persist across browser sessions.
|
||||
*
|
||||
* Requires COOP/COEP headers to be set by the server:
|
||||
@@ -354,9 +456,10 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
const mode = options.createIfNotExists !== false ? 'c' : '';
|
||||
this.db = new this.sqlite3!.oo1.OpfsDb(path, mode);
|
||||
this.opfsDbPath = path;
|
||||
this.sahPoolDbName = undefined;
|
||||
|
||||
// Configure the database for OPFS
|
||||
// Note: WAL mode requires exclusive locking in OPFS environment
|
||||
// Configure the database for legacy OPFS
|
||||
// Note: WAL mode is not supported by the legacy OPFS VFS
|
||||
this.db.exec("PRAGMA journal_mode = DELETE");
|
||||
this.db.exec("PRAGMA synchronous = NORMAL");
|
||||
|
||||
@@ -370,10 +473,10 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the currently open database is stored in OPFS.
|
||||
* Check if the currently open database is stored in OPFS (legacy or SAHPool).
|
||||
*/
|
||||
get isUsingOpfs(): boolean {
|
||||
return this.opfsDbPath !== undefined;
|
||||
return this.opfsDbPath !== undefined || this.sahPoolDbName !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,7 +484,7 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
* Returns undefined if not using OPFS.
|
||||
*/
|
||||
get currentOpfsPath(): string | undefined {
|
||||
return this.opfsDbPath;
|
||||
return this.opfsDbPath ?? this.sahPoolDbName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -426,7 +529,8 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
const startTime = performance.now();
|
||||
|
||||
this.db = new this.sqlite3!.oo1.DB(":memory:", "c");
|
||||
this.opfsDbPath = undefined; // Not using OPFS
|
||||
this.opfsDbPath = undefined;
|
||||
this.sahPoolDbName = undefined;
|
||||
this.db.exec("PRAGMA journal_mode = WAL");
|
||||
|
||||
const loadTime = performance.now() - startTime;
|
||||
@@ -448,7 +552,8 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
// once we swap connections. Drop them so callers re-prepare.
|
||||
this.clearStatementCache();
|
||||
this.db = new this.sqlite3!.oo1.DB({ filename: ":memory:", flags: "c" });
|
||||
this.opfsDbPath = undefined; // Not using OPFS
|
||||
this.opfsDbPath = undefined;
|
||||
this.sahPoolDbName = undefined;
|
||||
|
||||
const rc = this.sqlite3!.capi.sqlite3_deserialize(
|
||||
this.db.pointer!,
|
||||
@@ -592,8 +697,9 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
this.db = undefined;
|
||||
}
|
||||
|
||||
// Reset OPFS state
|
||||
// Reset OPFS / SAHPool state
|
||||
this.opfsDbPath = undefined;
|
||||
this.sahPoolDbName = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,6 +66,11 @@ export function startLocalServerWorker() {
|
||||
}
|
||||
|
||||
export function attachServiceWorkerBridge() {
|
||||
if (!("serviceWorker" in navigator) || !navigator.serviceWorker) {
|
||||
console.warn("[LocalBridge] Service workers not available — skipping bridge setup");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener("message", async (event) => {
|
||||
const msg = event.data;
|
||||
if (!msg || msg.type !== "LOCAL_FETCH") return;
|
||||
|
||||
@@ -122,6 +122,127 @@ async function writeOpfsFile(fileName: string, buffer: Uint8Array): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from the OPFS root into a Uint8Array.
|
||||
* Used during migration from legacy OPFS VFS to SAHPool.
|
||||
*/
|
||||
async function readOpfsFile(fileName: string): Promise<Uint8Array> {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
const fileHandle = await root.getFileHandle(fileName);
|
||||
const file = await fileHandle.getFile();
|
||||
return new Uint8Array(await file.arrayBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from the OPFS root.
|
||||
* Used to clean up the legacy OPFS database after migration to SAHPool.
|
||||
*/
|
||||
async function deleteOpfsFile(fileName: string): Promise<void> {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
await root.removeEntry(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a buffer contains a valid SQLite database by checking the
|
||||
* 16-byte magic string "SQLite format 3\0".
|
||||
*/
|
||||
function assertSqliteMagic(buffer: Uint8Array, source: string): void {
|
||||
const magic = new TextDecoder().decode(buffer.subarray(0, 15));
|
||||
if (magic !== "SQLite format 3") {
|
||||
throw new Error(
|
||||
`${source} is not a SQLite database ` +
|
||||
`(got ${buffer.byteLength} bytes starting with "${magic}"). ` +
|
||||
`The file is likely missing and the SPA fallback is returning index.html.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate database from legacy OPFS VFS to SAHPool VFS.
|
||||
* Checks if a legacy `/trilium.db` file exists in the OPFS root, and if the
|
||||
* SAHPool doesn't already have it. If migration is needed, the legacy file is
|
||||
* read, imported into the pool, and then deleted.
|
||||
*/
|
||||
async function migrateFromLegacyOpfs(dbName: string): Promise<void> {
|
||||
const legacyFileName = dbName.replace(/^\//, ""); // strip leading slash
|
||||
const legacyExists = await opfsFileExists(legacyFileName);
|
||||
|
||||
if (!legacyExists) {
|
||||
return; // Nothing to migrate
|
||||
}
|
||||
|
||||
// Check if SAHPool already has this DB (e.g. migration already happened)
|
||||
const poolFiles = sqlProvider!.sahPool!.getFileNames();
|
||||
if (poolFiles.includes(dbName)) {
|
||||
console.log("[Worker] SAHPool already contains the database, deleting legacy OPFS file...");
|
||||
await deleteOpfsFile(legacyFileName);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Worker] Migrating database from legacy OPFS to SAHPool VFS...");
|
||||
const startTime = performance.now();
|
||||
|
||||
const buffer = await readOpfsFile(legacyFileName);
|
||||
assertSqliteMagic(buffer, "Legacy OPFS database");
|
||||
|
||||
await sqlProvider!.sahPool!.importDb(dbName, buffer);
|
||||
await deleteOpfsFile(legacyFileName);
|
||||
|
||||
// Also clean up legacy journal/WAL files if they exist
|
||||
for (const suffix of ["-journal", "-wal", "-shm"]) {
|
||||
try {
|
||||
await deleteOpfsFile(legacyFileName + suffix);
|
||||
} catch {
|
||||
// Ignore — file may not exist
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = performance.now() - startTime;
|
||||
console.log(`[Worker] Migration complete in ${elapsed.toFixed(2)}ms (${buffer.byteLength} bytes)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the test fixture database for integration tests.
|
||||
* Seeds from the fixture if not already present, using SAHPool when available.
|
||||
*/
|
||||
async function loadTestDatabase(sahPoolAvailable: boolean, dbName: string): Promise<void> {
|
||||
if (sahPoolAvailable) {
|
||||
const poolFiles = sqlProvider!.sahPool!.getFileNames();
|
||||
if (!poolFiles.includes(dbName)) {
|
||||
console.log("[Worker] Integration test mode: seeding fixture database into SAHPool...");
|
||||
const buffer = await fetchTestFixture();
|
||||
await sqlProvider!.sahPool!.importDb(dbName, buffer);
|
||||
} else {
|
||||
console.log("[Worker] Integration test mode: reusing existing SAHPool DB from earlier in this test");
|
||||
}
|
||||
sqlProvider!.loadFromSahPool(dbName);
|
||||
} else {
|
||||
// Fallback to legacy OPFS for tests when SAHPool isn't available
|
||||
const legacyFileName = dbName.replace(/^\//, "");
|
||||
if (!(await opfsFileExists(legacyFileName))) {
|
||||
console.log("[Worker] Integration test mode: seeding fixture database into OPFS...");
|
||||
const buffer = await fetchTestFixture();
|
||||
await writeOpfsFile(legacyFileName, buffer);
|
||||
} else {
|
||||
console.log("[Worker] Integration test mode: reusing existing OPFS DB from earlier in this test");
|
||||
}
|
||||
sqlProvider!.loadFromOpfs(dbName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the test fixture database and validate it.
|
||||
*/
|
||||
async function fetchTestFixture(): Promise<Uint8Array> {
|
||||
const response = await fetch("/test-fixtures/document.db");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch test fixture: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const buffer = new Uint8Array(await response.arrayBuffer());
|
||||
assertSqliteMagic(buffer, "Test fixture at /test-fixtures/document.db");
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all required modules using dynamic imports.
|
||||
* This allows errors to be caught by our error handlers.
|
||||
@@ -193,10 +314,20 @@ async function initialize(): Promise<void> {
|
||||
console.log("[Worker] Initializing SQLite WASM...");
|
||||
await sqlProvider!.initWasm();
|
||||
|
||||
// Try to install the SAHPool VFS (preferred: supports WAL, much faster)
|
||||
let sahPoolAvailable = false;
|
||||
try {
|
||||
await sqlProvider!.installSahPool();
|
||||
sahPoolAvailable = true;
|
||||
} catch (e) {
|
||||
console.warn("[Worker] SAHPool VFS not available, will fall back to legacy OPFS or in-memory:", e);
|
||||
}
|
||||
|
||||
// Integration test mode is baked in at build time via the
|
||||
// __TRILIUM_INTEGRATION_TEST__ Vite define (derived from the
|
||||
// TRILIUM_INTEGRATION_TEST env var when the bundle was built).
|
||||
const integrationTestMode = __TRILIUM_INTEGRATION_TEST__;
|
||||
const dbName = "/trilium.db";
|
||||
|
||||
if (integrationTestMode === "memory") {
|
||||
// Use OPFS for the DB in integration test mode so option changes
|
||||
@@ -204,34 +335,16 @@ async function initialize(): Promise<void> {
|
||||
// Playwright gives each test a fresh BrowserContext, which means a
|
||||
// fresh OPFS — so on the first worker init of a test we seed from
|
||||
// the fixture, and subsequent inits in the same test reuse it.
|
||||
const opfsDbName = "trilium.db";
|
||||
if (!(await opfsFileExists(opfsDbName))) {
|
||||
console.log("[Worker] Integration test mode: seeding fixture database into OPFS...");
|
||||
const response = await fetch("/test-fixtures/document.db");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch test fixture: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const buffer = new Uint8Array(await response.arrayBuffer());
|
||||
// Verify we actually got a SQLite database, not an SPA-fallback
|
||||
// index.html served for a missing file. SQLite databases start
|
||||
// with the 16-byte magic string "SQLite format 3\0".
|
||||
const magic = new TextDecoder().decode(buffer.subarray(0, 15));
|
||||
if (magic !== "SQLite format 3") {
|
||||
throw new Error(
|
||||
`Test fixture at /test-fixtures/document.db is not a SQLite database ` +
|
||||
`(got ${buffer.byteLength} bytes starting with "${magic}"). ` +
|
||||
`The file is likely missing and the SPA fallback is returning index.html.`
|
||||
);
|
||||
}
|
||||
await writeOpfsFile(opfsDbName, buffer);
|
||||
} else {
|
||||
console.log("[Worker] Integration test mode: reusing existing OPFS DB from earlier in this test");
|
||||
}
|
||||
sqlProvider!.loadFromOpfs(`/${opfsDbName}`);
|
||||
await loadTestDatabase(sahPoolAvailable, dbName);
|
||||
} else if (sahPoolAvailable) {
|
||||
// SAHPool available — migrate from legacy OPFS if needed, then open
|
||||
await migrateFromLegacyOpfs(dbName);
|
||||
console.log("[Worker] SAHPool available, loading persistent database (WAL mode)...");
|
||||
sqlProvider!.loadFromSahPool(dbName);
|
||||
} else if (sqlProvider!.isOpfsAvailable()) {
|
||||
// Try to use OPFS for persistent storage
|
||||
console.log("[Worker] OPFS available, loading persistent database...");
|
||||
sqlProvider!.loadFromOpfs("/trilium.db");
|
||||
// Fall back to legacy OPFS VFS (no WAL, slower writes)
|
||||
console.warn("[Worker] Using legacy OPFS VFS (no WAL mode). Consider enabling COOP/COEP headers for SAHPool.");
|
||||
sqlProvider!.loadFromOpfs(dbName);
|
||||
} else {
|
||||
// Fall back to in-memory database (non-persistent)
|
||||
console.warn("[Worker] OPFS not available, using in-memory database (data will not persist)");
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { attachServiceWorkerBridge, startLocalServerWorker } from "./local-bridge.js";
|
||||
|
||||
async function waitForServiceWorkerControl(): Promise<void> {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
throw new Error("Service Worker not supported in this browser");
|
||||
if (!("serviceWorker" in navigator) || !navigator.serviceWorker) {
|
||||
const isSecure = location.protocol === "https:" || location.hostname === "localhost" || location.hostname === "127.0.0.1";
|
||||
const hints: string[] = [];
|
||||
if (!isSecure) {
|
||||
hints.push(`The page is served over ${location.protocol}//${location.hostname} which is not a secure context. Service workers require HTTPS (or localhost).`);
|
||||
}
|
||||
if (window.isSecureContext === false) {
|
||||
hints.push("The browser reports this is not a secure context.");
|
||||
}
|
||||
throw new Error(
|
||||
"Service workers are not available in this browser.\n\n" +
|
||||
"Trilium standalone mode requires service workers to function.\n" +
|
||||
(hints.length ? "\nPossible cause:\n- " + hints.join("\n- ") + "\n" : "") +
|
||||
"\nTo fix this, access the application over HTTPS or via localhost."
|
||||
);
|
||||
}
|
||||
|
||||
// If already controlling, we're good
|
||||
@@ -67,7 +80,7 @@ async function bootstrap() {
|
||||
<div style="padding: 40px; max-width: 600px; margin: 0 auto; font-family: system-ui, sans-serif;">
|
||||
<h1 style="color: #d32f2f;">Failed to Initialize</h1>
|
||||
<p>The application failed to start. Please check the browser console for details.</p>
|
||||
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow: auto;">${err instanceof Error ? err.message : String(err)}</pre>
|
||||
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow: auto; white-space: pre-wrap; word-wrap: break-word;">${err instanceof Error ? err.message : String(err)}</pre>
|
||||
<button onclick="location.reload()" style="padding: 12px 24px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">
|
||||
Reload Page
|
||||
</button>
|
||||
|
||||
@@ -91,6 +91,7 @@ export default class DesktopLayout {
|
||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget().class("full-width"))
|
||||
.optChild(glob.isStandalone, <StandaloneWarningBar />)
|
||||
.optChild(isNewLayout, <RightPaneToggle />)
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px")
|
||||
@@ -118,6 +119,7 @@ export default class DesktopLayout {
|
||||
.class("tab-row-container")
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget())
|
||||
.optChild(glob.isStandalone, <StandaloneWarningBar />)
|
||||
.optChild(isNewLayout, <RightPaneToggle />)
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px")
|
||||
@@ -187,7 +189,6 @@ export default class DesktopLayout {
|
||||
)
|
||||
)
|
||||
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||
.optChild(glob.isStandalone, <StandaloneWarningBar />)
|
||||
.child(<CloseZenModeButton />)
|
||||
|
||||
// Desktop-specific dialogs.
|
||||
|
||||
@@ -56,7 +56,6 @@ export default class MobileLayout {
|
||||
.child(
|
||||
new SplitNoteContainer(() =>
|
||||
new NoteWrapperWidget()
|
||||
.optChild(glob.isStandalone, <StandaloneWarningBar />)
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.class("title-row note-split-title")
|
||||
@@ -66,6 +65,7 @@ export default class MobileLayout {
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<NoteBadges />)
|
||||
.optChild(glob.isStandalone, <StandaloneWarningBar />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -100,11 +100,15 @@ body.setup {
|
||||
>.back-button {
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
left: 2em;
|
||||
inset-inline-start: 2em;
|
||||
color: var(--muted-text-color);
|
||||
|
||||
.tn-icon {
|
||||
margin-right: 0.4em;
|
||||
margin-inline-end: 0.4em;
|
||||
}
|
||||
|
||||
body[dir=rtl] & .tn-icon {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,19 +138,18 @@ body.setup {
|
||||
>.page-error {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset-inline: 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;
|
||||
padding-inline-end: 2.5em;
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
inset-inline-end: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +311,22 @@ body.setup.platform-darwin {
|
||||
animation-name: slide-in-left;
|
||||
}
|
||||
|
||||
body[dir=rtl] .slide-out-forward {
|
||||
animation-name: slide-out-right;
|
||||
}
|
||||
|
||||
body[dir=rtl] .slide-out-backward {
|
||||
animation-name: slide-out-left;
|
||||
}
|
||||
|
||||
body[dir=rtl] .slide-in-forward {
|
||||
animation-name: slide-in-left;
|
||||
}
|
||||
|
||||
body[dir=rtl] .slide-in-backward {
|
||||
animation-name: slide-in-right;
|
||||
}
|
||||
|
||||
.page.select-language {
|
||||
.dropdownWrapper {
|
||||
padding-bottom: 2em;
|
||||
@@ -394,7 +413,7 @@ body.setup.platform-darwin {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted-text-color);
|
||||
min-width: 2.5em;
|
||||
text-align: right;
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,8 @@ function SelectLanguage({ setState }: { setState: (state: State) => void }) {
|
||||
<FormList onSelect={async (id) => {
|
||||
await i18n.changeLanguage(id);
|
||||
setCurrentLocale(id);
|
||||
const locale = LOCALES.find(l => l.id === id);
|
||||
document.body.dir = locale?.rtl ? "rtl" : "ltr";
|
||||
}}>
|
||||
{filteredLocales.map(locale => (
|
||||
<FormListItem key={locale.id} value={locale.id} active={locale.id === currentLocale}>{locale.name}</FormListItem>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"about": {
|
||||
"standalone": {
|
||||
"badge_label": "Standalone",
|
||||
"warning_tooltip": "You are running Trilium in standalone mode. Some features are not available, and you may experience issues or data loss. Use the desktop application or self-hosted server for the best experience."
|
||||
},
|
||||
"about": {
|
||||
"version_label": "Version:",
|
||||
"version": "{{appVersion}} (database: {{dbVersion}}, sync protocol: {{syncVersion}})",
|
||||
"build_info": "Build: {{buildDate}}, revision: <buildRevision />",
|
||||
|
||||
20
apps/client/src/widgets/layout/StandaloneWarningBar.css
Normal file
20
apps/client/src/widgets/layout/StandaloneWarningBar.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.component.standalone-badge {
|
||||
contain: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
margin-inline: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
background-color: color-mix(in srgb, var(--color-warning, #e6a700) 15%, transparent);
|
||||
color: var(--color-warning, #e6a700);
|
||||
border: 1px solid color-mix(in srgb, var(--color-warning, #e6a700) 30%, transparent);
|
||||
|
||||
.bx {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,27 @@
|
||||
import { isMobile } from "../../services/utils";
|
||||
import Admonition from "../react/Admonition";
|
||||
import { useRef } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useNoteContext, useTooltip } from "../react/hooks";
|
||||
import "./StandaloneWarningBar.css";
|
||||
|
||||
export default function StandaloneWarningBar() {
|
||||
const { noteContext } = useNoteContext();
|
||||
const badgeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useTooltip(badgeRef, {
|
||||
title: t("standalone.warning_tooltip"),
|
||||
placement: "top",
|
||||
delay: 200
|
||||
});
|
||||
|
||||
// Only show in the main split, not sub-splits.
|
||||
if (noteContext?.mainNtxId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="standalone-warning-bar"
|
||||
style={{
|
||||
contain: "none"
|
||||
}}
|
||||
>
|
||||
<Admonition
|
||||
type="caution"
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "0.8em"
|
||||
}}
|
||||
>
|
||||
{isMobile()
|
||||
? "Running Trilium standalone. Beware of data loss and other issues."
|
||||
: "You are running Trilium in standalone mode. Some features are not available, and you may experience issues or data loss. Use the desktop application or self-hosted server for the best experience."
|
||||
}
|
||||
</Admonition>
|
||||
<div ref={badgeRef} className="standalone-badge">
|
||||
<span className="bx bx-error-circle" />
|
||||
<span className="standalone-badge-text">{t("standalone.badge_label")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user