Compare commits
42 Commits
6be377001e
...
09c7fb095d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c7fb095d | ||
|
|
f0b30c5e91 | ||
|
|
73e3196124 | ||
|
|
8a7bcc316e | ||
|
|
a2921cb982 | ||
|
|
29ce004974 | ||
|
|
026ba5ddce | ||
|
|
ab0585609a | ||
|
|
14a8bdb0c0 | ||
|
|
397d04dd88 | ||
|
|
fbb0bb7491 | ||
|
|
ee987dae99 | ||
|
|
720281a8db | ||
|
|
ff0c89e5a3 | ||
|
|
442937f540 | ||
|
|
dc01b787c1 | ||
|
|
c709b5d34c | ||
|
|
f8b386e42d | ||
|
|
a82f8ce3ad | ||
|
|
d31135bf21 | ||
|
|
75a77acefe | ||
|
|
ec915177ad | ||
|
|
5adee3e217 | ||
|
|
278d82645e | ||
|
|
e66f13b471 | ||
|
|
0e2955b57e | ||
|
|
2a4d5ec1ec | ||
|
|
07ce63de69 | ||
|
|
b539862eef | ||
|
|
ac4be3f8a8 | ||
|
|
0dc2d07b58 | ||
|
|
b097f9dc21 | ||
|
|
8aa4a97480 | ||
|
|
3e54d0ceae | ||
|
|
e1cec3404a | ||
|
|
e00e3999c5 | ||
|
|
2cbe96d815 | ||
|
|
0d8453f6a7 | ||
|
|
52b41b1bb0 | ||
|
|
c7265017b3 | ||
|
|
634e0b6d30 | ||
|
|
07f273fda4 |
@ -1,10 +1,15 @@
|
||||
{
|
||||
"name": "build-docs",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"description": "Build documentation from Trilium notes",
|
||||
"main": "src/main.ts",
|
||||
"bin": {
|
||||
"trilium-build-docs": "dist/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "tsx ."
|
||||
"start": "tsx .",
|
||||
"cli": "tsx src/cli.ts",
|
||||
"build": "tsx scripts/build.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
@ -14,6 +19,7 @@
|
||||
"@redocly/cli": "2.15.1",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"js-yaml": "4.1.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"typedoc": "0.28.16",
|
||||
|
||||
23
apps/build-docs/scripts/build.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import BuildHelper from "../../../scripts/build-utils";
|
||||
|
||||
const build = new BuildHelper("apps/build-docs");
|
||||
|
||||
async function main() {
|
||||
// Build the CLI and other TypeScript files
|
||||
await build.buildBackend([
|
||||
"src/cli.ts",
|
||||
"src/main.ts",
|
||||
"src/build-docs.ts",
|
||||
"src/swagger.ts",
|
||||
"src/script-api.ts",
|
||||
"src/context.ts"
|
||||
]);
|
||||
|
||||
// Copy HTML template
|
||||
build.copy("src/index.html", "index.html");
|
||||
|
||||
// Copy node modules dependencies if needed
|
||||
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]);
|
||||
}
|
||||
|
||||
main();
|
||||
@ -13,8 +13,12 @@
|
||||
* Make sure to keep in line with backend's `script_context.ts`.
|
||||
*/
|
||||
|
||||
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
|
||||
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
|
||||
export type {
|
||||
default as AbstractBeccaEntity
|
||||
} from "../../server/src/becca/entities/abstract_becca_entity.js";
|
||||
export type {
|
||||
default as BAttachment
|
||||
} from "../../server/src/becca/entities/battachment.js";
|
||||
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
|
||||
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
|
||||
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
|
||||
@ -31,6 +35,7 @@ export type { Api };
|
||||
const fakeNote = new BNote();
|
||||
|
||||
/**
|
||||
* The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
|
||||
* The `api` global variable allows access to the backend script API,
|
||||
* which is documented in {@link Api}.
|
||||
*/
|
||||
export const api: Api = new BackendScriptApi(fakeNote, {});
|
||||
|
||||
@ -1,19 +1,90 @@
|
||||
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
|
||||
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
|
||||
// Only set TRILIUM_RESOURCE_DIR if not already set (e.g., by Nix wrapper)
|
||||
if (!process.env.TRILIUM_RESOURCE_DIR) {
|
||||
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
|
||||
}
|
||||
process.env.NODE_ENV = "development";
|
||||
|
||||
import cls from "@triliumnext/server/src/services/cls.js";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import archiver from "archiver";
|
||||
import { execSync } from "child_process";
|
||||
import { WriteStream } from "fs";
|
||||
import * as fs from "fs/promises";
|
||||
import * as fsExtra from "fs-extra";
|
||||
import archiver from "archiver";
|
||||
import { WriteStream } from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import yaml from "js-yaml";
|
||||
import { dirname, join, resolve } from "path";
|
||||
|
||||
import BuildContext from "./context.js";
|
||||
|
||||
interface NoteMapping {
|
||||
rootNoteId: string;
|
||||
path: string;
|
||||
format: "markdown" | "html" | "share";
|
||||
ignoredFiles?: string[];
|
||||
exportOnly?: boolean;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
baseUrl: string;
|
||||
noteMappings: NoteMapping[];
|
||||
}
|
||||
|
||||
const DOCS_ROOT = "../../../docs";
|
||||
const OUTPUT_DIR = "../../site";
|
||||
|
||||
// Load configuration from edit-docs-config.yaml
|
||||
async function loadConfig(configPath?: string): Promise<Config | null> {
|
||||
const pathsToTry = configPath
|
||||
? [resolve(configPath)]
|
||||
: [
|
||||
join(process.cwd(), "edit-docs-config.yaml"),
|
||||
join(__dirname, "../../../edit-docs-config.yaml")
|
||||
];
|
||||
|
||||
for (const path of pathsToTry) {
|
||||
try {
|
||||
const configContent = await fs.readFile(path, "utf-8");
|
||||
const config = yaml.load(configContent) as Config;
|
||||
|
||||
// Resolve all paths relative to the config file's directory
|
||||
const CONFIG_DIR = dirname(path);
|
||||
config.noteMappings = config.noteMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
path: resolve(CONFIG_DIR, mapping.path)
|
||||
}));
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error; // rethrow unexpected errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No config file found
|
||||
}
|
||||
|
||||
async function exportDocs(
|
||||
noteId: string,
|
||||
format: "markdown" | "html" | "share",
|
||||
outputPath: string,
|
||||
ignoredFiles?: string[]
|
||||
) {
|
||||
const zipFilePath = `output-${noteId}.zip`;
|
||||
try {
|
||||
const { exportToZipFile } = (await import("@triliumnext/server/src/services/export/zip.js"))
|
||||
.default;
|
||||
await exportToZipFile(noteId, format, zipFilePath, {});
|
||||
|
||||
const ignoredSet = ignoredFiles ? new Set(ignoredFiles) : undefined;
|
||||
await extractZip(zipFilePath, outputPath, ignoredSet);
|
||||
} finally {
|
||||
if (await fsExtra.exists(zipFilePath)) {
|
||||
await fsExtra.rm(zipFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
||||
const note = await importData(sourcePath);
|
||||
|
||||
@ -21,15 +92,18 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
||||
const zipName = outputSubDir || "user-guide";
|
||||
const zipFilePath = `output-${zipName}.zip`;
|
||||
try {
|
||||
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
|
||||
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js"))
|
||||
.default;
|
||||
const branch = note.getParentBranches()[0];
|
||||
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
|
||||
"no-progress-reporting",
|
||||
"export",
|
||||
null
|
||||
);
|
||||
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js"))
|
||||
.default(
|
||||
"no-progress-reporting",
|
||||
"export",
|
||||
null
|
||||
);
|
||||
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
|
||||
await exportToZip(taskContext, branch, "share", fileOutputStream);
|
||||
const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js");
|
||||
await waitForStreamToFinish(fileOutputStream);
|
||||
|
||||
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
|
||||
@ -42,7 +116,7 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function buildDocsInner() {
|
||||
async function buildDocsInner(config?: Config) {
|
||||
const i18n = await import("@triliumnext/server/src/services/i18n.js");
|
||||
await i18n.initializeTranslations();
|
||||
|
||||
@ -53,18 +127,49 @@ async function buildDocsInner() {
|
||||
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
|
||||
await beccaLoader.beccaLoaded;
|
||||
|
||||
// Build User Guide
|
||||
console.log("Building User Guide...");
|
||||
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
|
||||
if (config) {
|
||||
// Config-based build (reads from edit-docs-config.yaml)
|
||||
console.log("Building documentation from config file...");
|
||||
|
||||
// Build Developer Guide
|
||||
console.log("Building Developer Guide...");
|
||||
await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide");
|
||||
// Import all non-export-only mappings
|
||||
for (const mapping of config.noteMappings) {
|
||||
if (!mapping.exportOnly) {
|
||||
console.log(`Importing from ${mapping.path}...`);
|
||||
await importData(mapping.path);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy favicon.
|
||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
|
||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico"));
|
||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
|
||||
// Export all mappings
|
||||
for (const mapping of config.noteMappings) {
|
||||
if (mapping.exportOnly) {
|
||||
console.log(`Exporting ${mapping.format} to ${mapping.path}...`);
|
||||
await exportDocs(
|
||||
mapping.rootNoteId,
|
||||
mapping.format,
|
||||
mapping.path,
|
||||
mapping.ignoredFiles
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy hardcoded build (for backward compatibility)
|
||||
console.log("Building User Guide...");
|
||||
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
|
||||
|
||||
console.log("Building Developer Guide...");
|
||||
await importAndExportDocs(
|
||||
join(__dirname, DOCS_ROOT, "Developer Guide"),
|
||||
"developer-guide"
|
||||
);
|
||||
|
||||
// Copy favicon.
|
||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
|
||||
join(OUTPUT_DIR, "favicon.ico"));
|
||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
|
||||
join(OUTPUT_DIR, "user-guide", "favicon.ico"));
|
||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
|
||||
join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
|
||||
}
|
||||
|
||||
console.log("Documentation built successfully!");
|
||||
}
|
||||
@ -91,12 +196,13 @@ async function createImportZip(path: string) {
|
||||
zlib: { level: 0 }
|
||||
});
|
||||
|
||||
console.log("Archive path is ", resolve(path))
|
||||
console.log("Archive path is ", resolve(path));
|
||||
archive.directory(path, "/");
|
||||
|
||||
const outputStream = fsExtra.createWriteStream(inputFile);
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js");
|
||||
await waitForStreamToFinish(outputStream);
|
||||
|
||||
try {
|
||||
@ -106,15 +212,15 @@ async function createImportZip(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function waitForStreamToFinish(stream: WriteStream) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
stream.on("finish", () => res());
|
||||
stream.on("error", (err) => rej(err));
|
||||
});
|
||||
}
|
||||
|
||||
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
|
||||
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
|
||||
export async function extractZip(
|
||||
zipFilePath: string,
|
||||
outputPath: string,
|
||||
ignoredFiles?: Set<string>
|
||||
) {
|
||||
const { readZipFile, readContent } = (await import(
|
||||
"@triliumnext/server/src/services/import/zip.js"
|
||||
));
|
||||
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
|
||||
// We ignore directories since they can appear out of order anyway.
|
||||
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
|
||||
@ -129,6 +235,27 @@ export async function extractZip(zipFilePath: string, outputPath: string, ignore
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildDocsFromConfig(configPath?: string, gitRootDir?: string) {
|
||||
const config = await loadConfig(configPath);
|
||||
|
||||
if (gitRootDir) {
|
||||
// Build the share theme if we have a gitRootDir (for Trilium project)
|
||||
execSync(`pnpm run --filter share-theme build`, {
|
||||
stdio: "inherit",
|
||||
cwd: gitRootDir
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger the actual build.
|
||||
await new Promise((res, rej) => {
|
||||
cls.init(() => {
|
||||
buildDocsInner(config ?? undefined)
|
||||
.catch(rej)
|
||||
.then(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default async function buildDocs({ gitRootDir }: BuildContext) {
|
||||
// Build the share theme.
|
||||
execSync(`pnpm run --filter share-theme build`, {
|
||||
|
||||
89
apps/build-docs/src/cli.ts
Normal file
@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { buildDocsFromConfig } from "./build-docs.js";
|
||||
|
||||
// Parse command-line arguments
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let configPath: string | undefined;
|
||||
let showHelp = false;
|
||||
let showVersion = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "--config" || args[i] === "-c") {
|
||||
configPath = args[i + 1];
|
||||
if (!configPath) {
|
||||
console.error("Error: --config/-c requires a path argument");
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip the next argument as it's the value
|
||||
} else if (args[i] === "--help" || args[i] === "-h") {
|
||||
showHelp = true;
|
||||
} else if (args[i] === "--version" || args[i] === "-v") {
|
||||
showVersion = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { configPath, showHelp, showVersion };
|
||||
}
|
||||
|
||||
function getVersion(): string {
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
const version = getVersion();
|
||||
console.log(`
|
||||
Usage: trilium-build-docs [options]
|
||||
|
||||
Options:
|
||||
-c, --config <path> Path to the configuration file
|
||||
(default: edit-docs-config.yaml in current directory)
|
||||
-h, --help Display this help message
|
||||
-v, --version Display version information
|
||||
|
||||
Description:
|
||||
Builds documentation from Trilium note structure and exports to various formats.
|
||||
Configuration file should be in YAML format with the following structure:
|
||||
|
||||
baseUrl: "https://example.com"
|
||||
noteMappings:
|
||||
- rootNoteId: "noteId123"
|
||||
path: "docs"
|
||||
format: "markdown"
|
||||
- rootNoteId: "noteId456"
|
||||
path: "public/docs"
|
||||
format: "share"
|
||||
exportOnly: true
|
||||
|
||||
Version: ${version}
|
||||
`);
|
||||
}
|
||||
|
||||
function printVersion() {
|
||||
const version = getVersion();
|
||||
console.log(version);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { configPath, showHelp, showVersion } = parseArgs();
|
||||
|
||||
if (showHelp) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
} else if (showVersion) {
|
||||
printVersion();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
await buildDocsFromConfig(configPath);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error building documentation:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@ -13,16 +13,19 @@
|
||||
* Make sure to keep in line with frontend's `script_context.ts`.
|
||||
*/
|
||||
|
||||
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
|
||||
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
|
||||
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
|
||||
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
|
||||
export type { default as FNote } from "../../client/src/entities/fnote.js";
|
||||
export type { Api } from "../../client/src/services/frontend_script_api.js";
|
||||
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
|
||||
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
|
||||
export type {
|
||||
default as NoteContextAwareWidget
|
||||
} from "../../client/src/widgets/note_context_aware_widget.js";
|
||||
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
|
||||
|
||||
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
|
||||
|
||||
//@ts-expect-error
|
||||
|
||||
// @ts-expect-error - FrontendScriptApi is not directly exportable as Api without this simulation.
|
||||
export const api: Api = new FrontendScriptApi();
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { join } from "path";
|
||||
import BuildContext from "./context";
|
||||
import buildSwagger from "./swagger";
|
||||
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
import buildDocs from "./build-docs";
|
||||
import BuildContext from "./context";
|
||||
import buildScriptApi from "./script-api";
|
||||
import buildSwagger from "./swagger";
|
||||
|
||||
const context: BuildContext = {
|
||||
gitRootDir: join(__dirname, "../../../"),
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { execSync } from "child_process";
|
||||
import BuildContext from "./context";
|
||||
import { join } from "path";
|
||||
|
||||
import BuildContext from "./context";
|
||||
|
||||
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
|
||||
// Generate types
|
||||
execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import BuildContext from "./context";
|
||||
import { join } from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { mkdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
import BuildContext from "./context";
|
||||
|
||||
interface BuildInfo {
|
||||
specPath: string;
|
||||
@ -27,6 +28,9 @@ export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
|
||||
const absSpecPath = join(gitRootDir, specPath);
|
||||
const targetDir = join(baseDir, outDir);
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
|
||||
execSync(
|
||||
`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`,
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": [],
|
||||
"include": [
|
||||
"scripts/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../server"
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"entryPoints": [
|
||||
"src/backend_script_entrypoint.ts"
|
||||
],
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"plugin": [
|
||||
"typedoc-plugin-missing-exports"
|
||||
]
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"entryPoints": [
|
||||
"src/frontend_script_entrypoint.ts"
|
||||
],
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"plugin": [
|
||||
"typedoc-plugin-missing-exports"
|
||||
]
|
||||
|
||||
@ -46,10 +46,6 @@ if (utils.isElectron()) {
|
||||
electronContextMenu.setupContextMenu();
|
||||
}
|
||||
|
||||
if (utils.isPWA()) {
|
||||
initPWATopbarColor();
|
||||
}
|
||||
|
||||
function initOnElectron() {
|
||||
const electron: typeof Electron = utils.dynamicRequire("electron");
|
||||
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
|
||||
@ -134,20 +130,3 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) {
|
||||
const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote;
|
||||
nativeTheme.themeSource = themeSource;
|
||||
}
|
||||
|
||||
function initPWATopbarColor() {
|
||||
const tracker = $("#background-color-tracker");
|
||||
|
||||
if (tracker.length) {
|
||||
const applyThemeColor = () => {
|
||||
let meta = $("meta[name='theme-color']");
|
||||
if (!meta.length) {
|
||||
meta = $(`<meta name="theme-color">`).appendTo($("head"));
|
||||
}
|
||||
meta.attr("content", tracker.css("color"));
|
||||
};
|
||||
|
||||
tracker.on("transitionend", applyThemeColor);
|
||||
applyThemeColor();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
#background-color-tracker {
|
||||
color: var(--main-background-color) !important;
|
||||
}
|
||||
|
||||
span.keyboard-shortcut,
|
||||
kbd {
|
||||
display: none;
|
||||
|
||||
@ -77,7 +77,6 @@ export default class MobileLayout {
|
||||
.child(<NoteTitleActions />)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||
.child(<SearchResult />)
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import treeService from "../services/tree.js";
|
||||
import froca from "../services/froca.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import server from "../services/server.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type { ContextMenuCommandData,FilteredCommandNames } from "../components/app_context.js";
|
||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import server from "../services/server.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
|
||||
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
|
||||
|
||||
@ -32,8 +32,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
|
||||
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
|
||||
const parentNoteId = this.node.getParent().data.noteId;
|
||||
|
||||
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
|
||||
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
|
||||
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers" || note?.noteId === "_lbMobileVisibleLaunchers";
|
||||
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers" || note?.noteId === "_lbMobileAvailableLaunchers";
|
||||
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
|
||||
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
|
||||
const isItem = isVisibleItem || isAvailableItem;
|
||||
|
||||
@ -410,6 +410,7 @@ body.desktop .tabulator-popup-container,
|
||||
|
||||
.dropdown-menu.static {
|
||||
box-shadow: unset;
|
||||
backdrop-filter: unset !important;
|
||||
}
|
||||
|
||||
.dropend .dropdown-toggle::after {
|
||||
@ -1553,8 +1554,14 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
|
||||
}
|
||||
|
||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu.mobile-bottom-menu.show {
|
||||
--dropdown-bottom: 0px;
|
||||
padding-bottom: calc(max(var(--menu-padding-size), env(safe-area-inset-bottom))) !important;
|
||||
}
|
||||
|
||||
#mobile-sidebar-container {
|
||||
|
||||
@ -57,12 +57,12 @@
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* SEARCH PAGE
|
||||
*/
|
||||
|
||||
/* Button bar */
|
||||
.search-definition-widget .search-setting-table tbody:last-child div {
|
||||
.search-definition-widget .search-setting-table .search-actions-container {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@ -143,7 +143,7 @@
|
||||
/*
|
||||
* OPTIONS PAGES
|
||||
*/
|
||||
|
||||
|
||||
:root {
|
||||
--options-card-min-width: 500px;
|
||||
--options-card-max-width: 900px;
|
||||
@ -335,4 +335,4 @@ nav.options-section-tabs + .options-section {
|
||||
|
||||
.etapi-options-section div {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -662,7 +662,8 @@
|
||||
"show-cheatsheet": "Show Cheatsheet",
|
||||
"toggle-zen-mode": "Zen Mode",
|
||||
"new-version-available": "New Update Available",
|
||||
"download-update": "Get Version {{latestVersion}}"
|
||||
"download-update": "Get Version {{latestVersion}}",
|
||||
"search_notes": "Search notes"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Exit Zen Mode"
|
||||
@ -761,7 +762,8 @@
|
||||
"note_revisions": "Note revisions",
|
||||
"error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'",
|
||||
"error_unrecognized_command": "Unrecognized command {{command}}",
|
||||
"backlinks": "Backlinks"
|
||||
"backlinks": "Backlinks",
|
||||
"content_language_switcher": "Content language: {{language}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Change note icon",
|
||||
@ -906,6 +908,7 @@
|
||||
"debug": "debug",
|
||||
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
|
||||
"action": "action",
|
||||
"option": "option",
|
||||
"search_button": "Search",
|
||||
"search_execute": "Search & Execute actions",
|
||||
"save_to_note": "Save to note",
|
||||
@ -2277,5 +2280,8 @@
|
||||
"title_one": "{{count}} tab",
|
||||
"title_other": "{{count}} tabs",
|
||||
"more_options": "More options"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Bookmarks"
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
.fixed-note-tree-container {
|
||||
height: 60%;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
overflow: auto;
|
||||
|
||||
.tree-wrapper {
|
||||
padding: 0;
|
||||
|
||||
@ -38,7 +38,7 @@ export default function NoteDetail() {
|
||||
const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
|
||||
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
|
||||
const widgetRequestId = useRef(0);
|
||||
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile();
|
||||
const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile");
|
||||
|
||||
const props: TypeWidgetProps = {
|
||||
note: note!,
|
||||
@ -216,7 +216,7 @@ export default function NoteDetail() {
|
||||
"fixed-tree": hasFixedTree
|
||||
})}
|
||||
>
|
||||
{hasFixedTree && <FixedTree />}
|
||||
{hasFixedTree && <FixedTree noteContext={noteContext} />}
|
||||
|
||||
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
|
||||
return <NoteDetailWrapper
|
||||
@ -232,8 +232,7 @@ export default function NoteDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
function FixedTree() {
|
||||
const { noteContext } = useNoteContext();
|
||||
function FixedTree({ noteContext }: { noteContext: NoteContext }) {
|
||||
const [ treeEl ] = useLegacyWidget(() => new NoteTreeWidget(), { noteContext });
|
||||
return <div class="fixed-note-tree-container">{treeEl}</div>;
|
||||
}
|
||||
|
||||
@ -45,6 +45,10 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
noDropdownListStyle
|
||||
mobileBackdrop
|
||||
>
|
||||
{isMobile() && <>
|
||||
<MenuItem command="searchNotes" icon="bx bx-search" text={t("global_menu.search_notes")} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||
<MenuItem command="showShareSubtree" icon="bx bx-share-alt" text={t("global_menu.show_shared_notes_subtree")} />
|
||||
@ -105,7 +109,7 @@ function BrowserOnlyOptions() {
|
||||
|
||||
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
|
||||
return <>
|
||||
<FormListHeader text="Development Options"></FormListHeader>
|
||||
<FormListHeader text="Development Options" />
|
||||
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
|
||||
{experimentalFeatures.map((feature) => (
|
||||
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
|
||||
|
||||
@ -10,12 +10,6 @@
|
||||
--card-padding: 0.6em;
|
||||
}
|
||||
|
||||
body.mobile .board-view {
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.board-view-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -26,6 +20,12 @@ body.mobile .board-view {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
body.mobile .board-view-container {
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.board-view-container .board-column {
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@ -18,14 +18,12 @@ import FlexContainer from "./flex_container.js";
|
||||
* - `#root-container.vertical-layout`, if the current layout is horizontal.
|
||||
*/
|
||||
export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
private originalViewportHeight: number;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super(isHorizontalLayout ? "column" : "row");
|
||||
|
||||
this.id("root-widget");
|
||||
this.css("height", "100dvh");
|
||||
this.originalViewportHeight = getViewportHeight();
|
||||
}
|
||||
|
||||
render(): JQuery<HTMLElement> {
|
||||
@ -40,6 +38,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
this.#setThemeCapabilities();
|
||||
this.#setLocaleAndDirection(options.get("locale"));
|
||||
this.#setExperimentalFeatures();
|
||||
this.#initPWATopbarColor();
|
||||
|
||||
return super.render();
|
||||
}
|
||||
@ -65,8 +64,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#onMobileResize() {
|
||||
const currentViewportHeight = getViewportHeight();
|
||||
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
|
||||
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// If viewport is significantly smaller, keyboard is likely open
|
||||
const isKeyboardOpened = windowHeight - viewportHeight > 150;
|
||||
|
||||
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
||||
}
|
||||
|
||||
@ -113,8 +116,23 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
document.body.lang = locale;
|
||||
document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
#initPWATopbarColor() {
|
||||
if (!utils.isPWA()) return;
|
||||
const tracker = $("#background-color-tracker");
|
||||
|
||||
if (tracker.length) {
|
||||
const applyThemeColor = () => {
|
||||
let meta = $("meta[name='theme-color']");
|
||||
if (!meta.length) {
|
||||
meta = $(`<meta name="theme-color">`).appendTo($("head"));
|
||||
}
|
||||
meta.attr("content", tracker.css("color"));
|
||||
};
|
||||
|
||||
tracker.on("transitionend", applyThemeColor);
|
||||
applyThemeColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getViewportHeight() {
|
||||
return window.visualViewport?.height ?? window.innerHeight;
|
||||
}
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import { useContext, useMemo } from "preact/hooks";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import { CSSProperties } from "preact";
|
||||
import type FNote from "../../entities/fnote";
|
||||
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
|
||||
import "./BookmarkButtons.css";
|
||||
|
||||
import { CSSProperties } from "preact";
|
||||
import { useContext, useMemo } from "preact/hooks";
|
||||
|
||||
import type FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../react/hooks";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { CustomNoteLauncher } from "./GenericButtons";
|
||||
import ResponsiveContainer from "../react/ResponsiveContainer";
|
||||
import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
const PARENT_NOTE_ID = "_lbBookmarks";
|
||||
|
||||
@ -19,17 +24,64 @@ export default function BookmarkButtons() {
|
||||
const childNotes = useChildNotes(PARENT_NOTE_ID);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
</div>
|
||||
)
|
||||
<ResponsiveContainer
|
||||
desktop={
|
||||
<div style={style}>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
</div>
|
||||
}
|
||||
mobile={
|
||||
<LaunchBarDropdownButton
|
||||
icon="bx bx-bookmark"
|
||||
title={t("bookmark_buttons.bookmarks")}
|
||||
>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
</LaunchBarDropdownButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleBookmark({ note }: { note: FNote }) {
|
||||
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
|
||||
return bookmarkFolder
|
||||
? <BookmarkFolder note={note} />
|
||||
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
|
||||
return <ResponsiveContainer
|
||||
desktop={
|
||||
bookmarkFolder
|
||||
? <BookmarkFolder note={note} />
|
||||
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
|
||||
}
|
||||
mobile={<MobileBookmarkItem noteId={note.noteId} bookmarkFolder={bookmarkFolder} />}
|
||||
/>;
|
||||
}
|
||||
|
||||
function MobileBookmarkItem({ noteId, bookmarkFolder }: { noteId: string, bookmarkFolder: boolean }) {
|
||||
const note = useNote(noteId);
|
||||
const noteIcon = useNoteIcon(note);
|
||||
if (!note) return null;
|
||||
|
||||
return (
|
||||
!bookmarkFolder
|
||||
? <FormListItem icon={noteIcon} onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: note, getTargetNoteId: () => note.noteId })}>{note.title}</FormListItem>
|
||||
: <MobileBookmarkFolder note={note} />
|
||||
);
|
||||
}
|
||||
|
||||
function MobileBookmarkFolder({ note }: { note: FNote }) {
|
||||
const childNotes = useChildNotes(note.noteId);
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu icon="bx bx-folder" title={note.title}>
|
||||
{childNotes.map(childNote => (
|
||||
<FormListItem
|
||||
key={childNote.noteId}
|
||||
icon={childNote.getIcon()}
|
||||
onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: childNote, getTargetNoteId: () => childNote.noteId })}
|
||||
>
|
||||
{childNote.title}
|
||||
</FormListItem>
|
||||
))}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function BookmarkFolder({ note }: { note: FNote }) {
|
||||
@ -55,5 +107,5 @@ function BookmarkFolder({ note }: { note: FNote }) {
|
||||
</ul>
|
||||
</div>
|
||||
</LaunchBarDropdownButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,32 +7,18 @@ import { isCtrlKey } from "../../services/utils";
|
||||
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
|
||||
export function CustomNoteLauncher(props: {
|
||||
launcherNote: FNote;
|
||||
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
|
||||
getHoistedNoteId?: (launcherNote: FNote) => string | null;
|
||||
keyboardShortcut?: string;
|
||||
}) {
|
||||
const { launcherNote, getTargetNoteId } = props;
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
|
||||
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
|
||||
if (evt.which === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (!targetNoteId) return;
|
||||
|
||||
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
const ctrlKey = isCtrlKey(evt);
|
||||
|
||||
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
|
||||
const activate = !!evt.shiftKey;
|
||||
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
|
||||
} else {
|
||||
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
|
||||
}
|
||||
}, [ launcherNote, getTargetNoteId, getHoistedNoteId ]);
|
||||
await launchCustomNoteLauncher(evt, props);
|
||||
}, [ props ]);
|
||||
|
||||
// Keyboard shortcut.
|
||||
const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut");
|
||||
@ -54,3 +40,24 @@ export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNo
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function launchCustomNoteLauncher(evt: MouseEvent | KeyboardEvent, { launcherNote, getTargetNoteId, getHoistedNoteId }: {
|
||||
launcherNote: FNote;
|
||||
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
|
||||
getHoistedNoteId?: (launcherNote: FNote) => string | null;
|
||||
}) {
|
||||
if (evt.which === 3) return;
|
||||
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (!targetNoteId) return;
|
||||
|
||||
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
const ctrlKey = isCtrlKey(evt);
|
||||
|
||||
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
|
||||
const activate = !!evt.shiftKey;
|
||||
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
|
||||
} else {
|
||||
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
|
||||
placement: isHorizontalLayout ? "bottom" : "right"
|
||||
}
|
||||
}}
|
||||
mobileBackdrop
|
||||
{...props}
|
||||
>{children}</Dropdown>
|
||||
);
|
||||
|
||||
@ -50,6 +50,9 @@ body.experimental-feature-new-layout {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body.desktop .title-actions {
|
||||
> .collapsible,
|
||||
> .note-type-switcher {
|
||||
padding-inline-start: calc(24px - var(--title-actions-padding-start));
|
||||
@ -57,3 +60,4 @@ body.experimental-feature-new-layout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export default function NoteTitleActions() {
|
||||
<PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />
|
||||
{noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />}
|
||||
<EditedNotes />
|
||||
{viewScope?.viewMode === "default" && <NoteTypeSwitcher />}
|
||||
{(!viewScope?.viewMode || viewScope.viewMode === "default") && <NoteTypeSwitcher />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -58,33 +58,6 @@
|
||||
|
||||
.dropdown-note-info {
|
||||
padding: 1em !important;
|
||||
|
||||
ul {
|
||||
--row-block-margin: .2em;
|
||||
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: calc(0px - var(--row-block-margin));
|
||||
margin-bottom: 12px;
|
||||
display: table;
|
||||
|
||||
li {
|
||||
display: table-row;
|
||||
|
||||
> strong {
|
||||
display: table-cell;
|
||||
padding: var(--row-block-margin) 0;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
user-select: text;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-note-paths {
|
||||
@ -144,16 +117,50 @@
|
||||
|
||||
}
|
||||
|
||||
div.similar-notes-widget div.similar-notes-wrapper {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
button.select-button:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .note-info-content {
|
||||
ul {
|
||||
--row-block-margin: .2em;
|
||||
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: calc(0px - var(--row-block-margin));
|
||||
margin-bottom: 12px;
|
||||
display: table;
|
||||
|
||||
li {
|
||||
display: table-row;
|
||||
|
||||
> strong {
|
||||
display: table-cell;
|
||||
padding: var(--row-block-margin) 0;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
user-select: text;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout div.similar-notes-widget div.similar-notes-wrapper {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout.mobile div.similar-notes-widget div.similar-notes-wrapper {
|
||||
max-height: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
|
||||
@ -212,8 +212,8 @@ export function getLocaleName(locale: Locale | null | undefined) {
|
||||
|
||||
//#region Note info & Similar
|
||||
interface NoteInfoContext extends StatusBarContext {
|
||||
similarNotesShown: boolean;
|
||||
setSimilarNotesShown: (value: boolean) => void;
|
||||
similarNotesShown?: boolean;
|
||||
setSimilarNotesShown?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function NoteInfoBadge(context: NoteInfoContext) {
|
||||
@ -225,7 +225,7 @@ export function NoteInfoBadge(context: NoteInfoContext) {
|
||||
|
||||
// Keyboard shortcut.
|
||||
useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show());
|
||||
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown));
|
||||
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown && setSimilarNotesShown(!similarNotesShown));
|
||||
|
||||
return (enabled &&
|
||||
<StatusBarDropdown
|
||||
@ -242,8 +242,8 @@ export function NoteInfoBadge(context: NoteInfoContext) {
|
||||
);
|
||||
}
|
||||
|
||||
function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: NoteInfoContext & {
|
||||
dropdownRef: RefObject<BootstrapDropdown>;
|
||||
export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: Pick<NoteInfoContext, "note" | "setSimilarNotesShown"> & {
|
||||
dropdownRef?: RefObject<BootstrapDropdown>;
|
||||
noteType: NoteType;
|
||||
}) {
|
||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||
@ -251,7 +251,7 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
|
||||
const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="note-info-content">
|
||||
<ul>
|
||||
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
|
||||
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
||||
@ -262,14 +262,14 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
|
||||
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
|
||||
</ul>
|
||||
|
||||
<LinkButton
|
||||
{setSimilarNotesShown && <LinkButton
|
||||
text={t("note_info_widget.show_similar_notes")}
|
||||
onClick={() => {
|
||||
dropdownRef.current?.hide();
|
||||
dropdownRef?.current?.hide();
|
||||
setSimilarNotesShown(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
.code-note-switcher-modal .dropdown-menu {
|
||||
background: none !important;
|
||||
}
|
||||
@ -1,17 +1,24 @@
|
||||
import "./mobile_detail_menu.css";
|
||||
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { createPortal, useRef, useState } from "preact/compat";
|
||||
|
||||
import FNote, { NotePathRecord } from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import note_create from "../../services/note_create";
|
||||
import server from "../../services/server";
|
||||
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
||||
import { getLocaleName, NoteInfoContent } from "../layout/StatusBar";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useNoteContext } from "../react/hooks";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import { useNoteContext, useNoteProperty } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import { NoteTypeCodeNoteList, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
|
||||
import { NoteContextMenu } from "../ribbon/NoteActions";
|
||||
import NoteActionsCustom from "../ribbon/NoteActionsCustom";
|
||||
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
|
||||
import SimilarNotesTab from "../ribbon/SimilarNotesTab";
|
||||
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
||||
|
||||
export default function MobileDetailMenu() {
|
||||
const dropdownRef = useRef<BootstrapDropdown | null>(null);
|
||||
@ -20,6 +27,9 @@ export default function MobileDetailMenu() {
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const [ backlinksModalShown, setBacklinksModalShown ] = useState(false);
|
||||
const [ notePathsModalShown, setNotePathsModalShown ] = useState(false);
|
||||
const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false);
|
||||
const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false);
|
||||
const [ codeNoteSwitcherModalShown, setCodeNoteSwitcherModalShown ] = useState(false);
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
|
||||
@ -36,7 +46,7 @@ export default function MobileDetailMenu() {
|
||||
<NoteContextMenu
|
||||
dropdownRef={dropdownRef}
|
||||
note={note} noteContext={noteContext}
|
||||
extraItems={<>
|
||||
itemsAtStart={<>
|
||||
<div className="form-list-row">
|
||||
<div className="form-list-col">
|
||||
<FormListItem
|
||||
@ -81,6 +91,13 @@ export default function MobileDetailMenu() {
|
||||
</>}
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
itemsNearNoteSettings={<>
|
||||
{note.type === "text" && <ContentLanguageSelector note={note} />}
|
||||
{note.type === "code" && <FormListItem icon={"bx bx-code"} onClick={() => setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}</FormListItem>}
|
||||
<FormListItem icon="bx bx-info-circle" onClick={() => setNoteInfoModalShown(true)}>{t("note_info_widget.title")}</FormListItem>
|
||||
<FormListItem icon="bx bx-bar-chart" onClick={() => setSimilarNotesModalShown(true)}>{t("similar_notes.title")}</FormListItem>
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
@ -94,13 +111,46 @@ export default function MobileDetailMenu() {
|
||||
<>
|
||||
<BacklinksModal note={note} modalShown={backlinksModalShown} setModalShown={setBacklinksModalShown} />
|
||||
<NotePathsModal note={note} modalShown={notePathsModalShown} notePath={noteContext?.notePath} sortedNotePaths={sortedNotePaths} setModalShown={setNotePathsModalShown} />
|
||||
<NoteInfoModal note={note} modalShown={noteInfoModalShown} setModalShown={setNoteInfoModalShown} />
|
||||
<SimilarNotesModal note={note} modalShown={similarNotesModalShown} setModalShown={setSimilarNotesModalShown} />
|
||||
<CodeNoteSwitcherModal note={note} modalShown={codeNoteSwitcherModalShown} setModalShown={setCodeNoteSwitcherModalShown} />
|
||||
</>
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) {
|
||||
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
||||
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu
|
||||
icon="bx bx-globe"
|
||||
title={t("mobile_detail_menu.content_language_switcher", { language: getLocaleName(activeLocale ?? DEFAULT_LOCALE) })}
|
||||
>
|
||||
{processedLocales.map((locale, index) =>
|
||||
(typeof locale === "object") ? (
|
||||
<FormListItem
|
||||
key={locale.id}
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentNoteLanguage}
|
||||
onClick={() => setCurrentNoteLanguage(locale.id)}
|
||||
>{locale.name}</FormListItem>
|
||||
) : (
|
||||
<FormDropdownDivider key={`divider-${index}`} />
|
||||
)
|
||||
)}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
interface WithModal {
|
||||
modalShown: boolean;
|
||||
setModalShown: (shown: boolean) => void;
|
||||
}
|
||||
|
||||
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="backlinks-modal tn-backlinks-widget"
|
||||
@ -116,7 +166,7 @@ function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | nul
|
||||
);
|
||||
}
|
||||
|
||||
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined, setModalShown: (shown: boolean) => void }) {
|
||||
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="note-paths-modal"
|
||||
@ -134,3 +184,57 @@ function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalS
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="note-info-modal"
|
||||
size="md"
|
||||
title={t("note_info_widget.title")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
{note && <NoteInfoContent note={note} noteType={note.type} />}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
return (
|
||||
<Modal
|
||||
className="similar-notes-modal"
|
||||
size="md"
|
||||
title={t("similar_notes.title")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
<SimilarNotesTab note={note} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeNoteSwitcherModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
|
||||
const currentNoteMime = useNoteProperty(note, "mime");
|
||||
const mimeTypes = useMimeTypes();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="code-note-switcher-modal"
|
||||
size="md"
|
||||
title={t("status_bar.code_note_switcher")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
<div className="dropdown-menu static show">
|
||||
{note && <NoteTypeCodeNoteList
|
||||
currentMimeType={currentNoteMime}
|
||||
mimeTypes={mimeTypes}
|
||||
changeNoteType={(type, mime) => {
|
||||
server.put(`notes/${note.noteId}/type`, { type, mime });
|
||||
setModalShown(false);
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
12
apps/client/src/widgets/react/ResponsiveContainer.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
import { isMobile } from "../../services/utils";
|
||||
|
||||
interface ResponsiveContainerProps {
|
||||
mobile?: ComponentChildren;
|
||||
desktop?: ComponentChildren;
|
||||
}
|
||||
|
||||
export default function ResponsiveContainer({ mobile, desktop }: ResponsiveContainerProps) {
|
||||
return (isMobile() ? mobile : desktop);
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
@ -117,19 +116,18 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
|
||||
onClick={() => changeNoteType(type, mime)}
|
||||
>{title}</FormListItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
checked={checked}
|
||||
disabled
|
||||
>
|
||||
<strong>{title}</strong>
|
||||
</FormListItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
checked={checked}
|
||||
disabled
|
||||
>
|
||||
<strong>{title}</strong>
|
||||
</FormListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
|
||||
{!noCodeNotes && <NoteTypeCodeNoteList mimeTypes={mimeTypes} changeNoteType={changeNoteType} setModalShown={setModalShown} />}
|
||||
@ -141,7 +139,7 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp
|
||||
currentMimeType?: string;
|
||||
mimeTypes: MimeType[];
|
||||
changeNoteType(type: NoteType, mime: string): void;
|
||||
setModalShown(shown: boolean): void;
|
||||
setModalShown?(shown: boolean): void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@ -155,8 +153,10 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp
|
||||
</FormListItem>
|
||||
))}
|
||||
|
||||
<FormDropdownDivider />
|
||||
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
|
||||
{setModalShown && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
|
||||
</>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -195,7 +195,7 @@ function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
|
||||
onChange={(shouldProtect) => note && protected_session.protectNote(note.noteId, shouldProtect, false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EditabilitySelect({ note }: { note?: FNote | null }) {
|
||||
@ -417,9 +417,9 @@ function findTypeTitle(type?: NoteType, mime?: string | null) {
|
||||
const found = mimeTypes.find((mt) => mt.mime === mime);
|
||||
|
||||
return found ? found.title : mime;
|
||||
} else {
|
||||
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
|
||||
|
||||
return noteType ? noteType.title : type;
|
||||
}
|
||||
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
|
||||
|
||||
return noteType ? noteType.title : type;
|
||||
|
||||
}
|
||||
|
||||
@ -63,10 +63,11 @@ function RevisionsButton({ note }: { note: FNote }) {
|
||||
|
||||
type ItemToFocus = "basic-properties";
|
||||
|
||||
export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: externalDropdownRef }: {
|
||||
export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNoteSettings, dropdownRef: externalDropdownRef }: {
|
||||
note: FNote,
|
||||
noteContext?: NoteContext,
|
||||
extraItems?: ComponentChildren;
|
||||
itemsAtStart?: ComponentChildren;
|
||||
itemsNearNoteSettings?: ComponentChildren;
|
||||
dropdownRef?: RefObject<BootstrapDropdown>;
|
||||
}) {
|
||||
const dropdownRef = useSyncedRef<BootstrapDropdown>(externalDropdownRef, null);
|
||||
@ -112,7 +113,7 @@ export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: ex
|
||||
onHidden={() => itemToFocusRef.current = null }
|
||||
mobileBackdrop
|
||||
>
|
||||
{extraItems}
|
||||
{itemsAtStart}
|
||||
|
||||
{isReadOnly && <>
|
||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||
@ -131,6 +132,8 @@ export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: ex
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
{itemsNearNoteSettings}
|
||||
|
||||
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
||||
disabled={isInOptionsOrHelp || note.type === "search"}
|
||||
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
||||
|
||||
@ -2,6 +2,7 @@ import "./SearchDefinitionTab.css";
|
||||
|
||||
import { SaveSearchNoteResponse } from "@triliumnext/commons";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
@ -15,12 +16,13 @@ import tree from "../../services/tree";
|
||||
import { getErrorMessage } from "../../services/utils";
|
||||
import ws from "../../services/ws";
|
||||
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
||||
import Button from "../react/Button";
|
||||
import Button, { SplitButton } from "../react/Button";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormListHeader, FormListItem } from "../react/FormList";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import ResponsiveContainer from "../react/ResponsiveContainer";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
|
||||
|
||||
@ -84,15 +86,32 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
|
||||
<tr>
|
||||
<td className="title-column">{t("search_definition.add_search_option")}</td>
|
||||
<td colSpan={2} className="add-search-option">
|
||||
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
||||
<Button
|
||||
size="small"
|
||||
icon={icon}
|
||||
text={label}
|
||||
title={tooltip}
|
||||
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
||||
/>
|
||||
))}
|
||||
<ResponsiveContainer
|
||||
desktop={searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
||||
<Button
|
||||
key={`${attributeType}-${attributeName}`}
|
||||
size="small" icon={icon} text={label} title={tooltip}
|
||||
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
||||
/>
|
||||
))}
|
||||
mobile={
|
||||
<Dropdown
|
||||
buttonClassName="action-add-toggle btn btn-sm"
|
||||
text={<><Icon icon="bx bx-plus" />{" "}{t("search_definition.option")}</>}
|
||||
dropdownContainerClassName="mobile-bottom-menu" mobileBackdrop
|
||||
noSelectButtonStyle
|
||||
>
|
||||
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
||||
<FormListItem
|
||||
key={`${attributeType}-${attributeName}`}
|
||||
icon={icon}
|
||||
description={tooltip}
|
||||
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
||||
>{label}</FormListItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
|
||||
<AddBulkActionButton note={note} />
|
||||
</td>
|
||||
@ -113,48 +132,7 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
|
||||
})}
|
||||
</tbody>
|
||||
<BulkActionsList note={note} />
|
||||
<tbody className="search-actions">
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<div className="search-actions-container">
|
||||
<Button
|
||||
icon="bx bx-search"
|
||||
text={t("search_definition.search_button")}
|
||||
keyboardShortcut="Enter"
|
||||
onClick={refreshResults}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon="bx bxs-zap"
|
||||
text={t("search_definition.search_execute")}
|
||||
onClick={async () => {
|
||||
await server.post(`search-and-execute-note/${note.noteId}`);
|
||||
refreshResults();
|
||||
toast.showMessage(t("search_definition.actions_executed"), 3000);
|
||||
}}
|
||||
/>
|
||||
|
||||
{note.isHiddenCompletely() && <Button
|
||||
icon="bx bx-save"
|
||||
text={t("search_definition.save_to_note")}
|
||||
onClick={async () => {
|
||||
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||
|
||||
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
|
||||
// See https://www.i18next.com/translation-function/interpolation#unescape
|
||||
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<SearchButtonBar note={note} refreshResults={refreshResults} />
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
@ -162,6 +140,56 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
|
||||
);
|
||||
}
|
||||
|
||||
function SearchButtonBar({ note, refreshResults }: {
|
||||
note: FNote;
|
||||
refreshResults(): void;
|
||||
}) {
|
||||
async function searchAndExecuteActions() {
|
||||
await server.post(`search-and-execute-note/${note.noteId}`);
|
||||
refreshResults();
|
||||
toast.showMessage(t("search_definition.actions_executed"), 3000);
|
||||
}
|
||||
|
||||
async function saveSearchNote() {
|
||||
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
|
||||
if (!notePath) return;
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||
|
||||
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
|
||||
// See https://www.i18next.com/translation-function/interpolation#unescape
|
||||
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody className="search-actions">
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<ResponsiveContainer
|
||||
desktop={
|
||||
<div className="search-actions-container">
|
||||
<Button icon="bx bx-search" text={t("search_definition.search_button")} keyboardShortcut="Enter" onClick={refreshResults} />
|
||||
<Button icon="bx bxs-zap" text={t("search_definition.search_execute")} onClick={searchAndExecuteActions} />
|
||||
{note.isHiddenCompletely() && <Button icon="bx bx-save" text={t("search_definition.save_to_note")} onClick={saveSearchNote} />}
|
||||
</div>
|
||||
}
|
||||
mobile={
|
||||
<SplitButton
|
||||
text={t("search_definition.search_button")} icon="bx bx-search"
|
||||
onClick={refreshResults}
|
||||
>
|
||||
<FormListItem icon="bx bxs-zap" onClick={searchAndExecuteActions}>{t("search_definition.search_execute")}</FormListItem>
|
||||
{note.isHiddenCompletely() && <FormListItem icon="bx bx-save" onClick={saveSearchNote}>{t("search_definition.save_to_note")}</FormListItem>}
|
||||
</SplitButton>
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
function BulkActionsList({ note }: { note: FNote }) {
|
||||
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
|
||||
|
||||
@ -194,15 +222,18 @@ function AddBulkActionButton({ note }: { note: FNote }) {
|
||||
buttonClassName="action-add-toggle btn btn-sm"
|
||||
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
|
||||
noSelectButtonStyle
|
||||
dropdownContainerClassName="mobile-bottom-menu" mobileBackdrop
|
||||
>
|
||||
{ACTION_GROUPS.map(({ actions, title }) => (
|
||||
<>
|
||||
{ACTION_GROUPS.map(({ actions, title }, index) => (
|
||||
<Fragment key={index}>
|
||||
<FormListHeader text={title} />
|
||||
|
||||
{actions.map(({ actionName, actionTitle }) => (
|
||||
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
|
||||
))}
|
||||
</>
|
||||
<div>
|
||||
{actions.map(({ actionName, actionTitle }) => (
|
||||
<FormListItem key={actionName} onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@ -10,6 +10,12 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
|
||||
@media (max-width: 991px) {
|
||||
margin-block: 1em;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@ -42,6 +48,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
|
||||
@media (max-width: 991px) {
|
||||
gap: 0.5em;
|
||||
flex-wrap: wrap;
|
||||
.attachment-title { flex-grow: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-details {
|
||||
@ -127,10 +139,6 @@
|
||||
|
||||
/* #region Attachment actions */
|
||||
|
||||
.attachment-actions .dropdown-menu {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.attachment-actions .dropdown-item .tn-icon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
|
||||
@ -239,6 +239,8 @@ function AttachmentActions({ attachment, copyAttachmentLinkToClipboard }: { atta
|
||||
text={<Icon icon="bx bx-dots-vertical-rounded" />}
|
||||
buttonClassName="icon-action-always-border"
|
||||
iconAction
|
||||
dropdownContainerClassName="mobile-bottom-menu"
|
||||
mobileBackdrop
|
||||
>
|
||||
<FormListItem
|
||||
icon="bx bx-file-find"
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
<p>To easily access selected notes, you can bookmark them. See demo:</p>
|
||||
<p>
|
||||
<img src="Bookmarks_bookmarks.gif">
|
||||
</p>
|
||||
<p>Frequently used notes can be bookmarked, which will make them appear in
|
||||
the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a> for
|
||||
easy access.</p>
|
||||
<h2>Configuring the launch bar</h2>
|
||||
<p>If bookmarks don't appear in the launch bar, then most likely the bookmark
|
||||
section has been hidden. Go to the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a> configuration
|
||||
from the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a> and
|
||||
ensure <em>Bookmarks</em> is in the <em>Visible Launchers</em> section.</p>
|
||||
<h2>Bookmark folder</h2>
|
||||
<p>Space in the left panel is limited, and you might want to bookmark many
|
||||
items. One possible solution is to bookmark a folder, so it shows its children:</p>
|
||||
<p>
|
||||
<img src="Bookmarks_bookmark-folder.png">
|
||||
</p>
|
||||
<p>To do this, you need to add a <code spellcheck="false">#bookmarkFolder</code> label
|
||||
to the note.</p>
|
||||
<p>To do this, bookmark a folder and assign it the <code spellcheck="false">#bookmarkFolder</code> label.</p>
|
||||
<h2>Mobile</h2>
|
||||
<p>On mobile, bookmarks are only displayed starting with v0.102.0. Because
|
||||
of the more constrained screen size, the bookmarks are grouped under a
|
||||
single icon instead of displaying them as separate icons.</p>
|
||||
<p>When pressed, a menu will appear listing all the bookmarks. Bookmark folders
|
||||
are also supported and will appear as sub-menus.</p>
|
||||
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 305 KiB |
@ -1,8 +1,11 @@
|
||||
<h2>Position of the Launch bar</h2>
|
||||
<p>Depending on the layout selected, the launcher bar will either be on the
|
||||
left side of the screen with buttons displayed vertically or at the top
|
||||
of the screen. See <a href="#root/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a> for
|
||||
<p>On desktop, depending on the layout selected, the launcher bar will either
|
||||
be on the left side of the screen with buttons displayed vertically or
|
||||
at the top of the screen. See <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a> for
|
||||
more information.</p>
|
||||
<p>On mobile, the launch bar will always be at the bottom.</p>
|
||||
<p>If there are too many items in the launch bar to fit the screen, it will
|
||||
become scrollable.</p>
|
||||
<h2>Terminology</h2>
|
||||
<ul>
|
||||
<li><strong>Launcher</strong>: a button that can be (or is) displayed on the
|
||||
@ -12,16 +15,16 @@
|
||||
<li><strong>Visible Launcher</strong>: a launcher that is currently displayed
|
||||
on the launch bar.</li>
|
||||
</ul>
|
||||
<h2>Configuring the Launch bar</h2>
|
||||
<h2>Configuring the desktop Launch bar</h2>
|
||||
<p>There are two ways to configure the launch bar:</p>
|
||||
<ul>
|
||||
<li>Right click in the empty space between launchers on the launch bar and
|
||||
select <em>Configure Launchbar.</em>
|
||||
</li>
|
||||
<li>Click on the <a href="#root/_help_x3i7MxGccDuM">Global menu</a> and
|
||||
<li>Click on the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a> and
|
||||
select <em>Configure Launchbar</em>.</li>
|
||||
</ul>
|
||||
<p>This will open a new tab with the <a href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> listing
|
||||
<p>This will open a new tab with the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> listing
|
||||
the launchers.</p>
|
||||
<p>
|
||||
<img src="Launch Bar_image.png">
|
||||
@ -29,6 +32,19 @@
|
||||
<p>Expanding <em>Available Launchers</em> section will show the list of launchers
|
||||
that are not displayed on the launch bar. The <em>Visible Launchers</em> will
|
||||
show the ones that are currently displayed.</p>
|
||||
<h2>Configuring the mobile launch bar</h2>
|
||||
<p>The launch bar on mobile uses a different configuration from the desktop
|
||||
one. The reasoning is that not all desktop icons are available on mobile,
|
||||
and fewer icons fit on a mobile screen.</p>
|
||||
<p>To configure the launch bar on mobile, go to <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a> and
|
||||
select <em>Configure Launchbar</em>.</p>
|
||||
<p>The configure the mobile launch bar while on the desktop (especially useful
|
||||
to configure more complicated launchers such as scripts or custom widgets),
|
||||
go to <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a> →
|
||||
Advanced → Show Hidden Subtree and look for the <em>Mobile Launch Bar</em> section.
|
||||
While in the hidden subtree, it's also possible to drag launchers between
|
||||
the <em>Mobile Launch Bar</em> and (Desktop) <em>Launch Bar</em> sections.</p>
|
||||
<h3>Adding/removing and reordering launchers</h3>
|
||||
<p>To display a new launcher in the launch bar, first look for it in the <em>Available Launchers</em> section.
|
||||
Then right click it and select <em>Move to visible launchers</em>. It is
|
||||
@ -36,13 +52,13 @@
|
||||
<p>Similarly, to remove it from the launch bar, simply look for it in <em>Visible Launchers</em> then
|
||||
right click it and select <em>Move to available launchers</em> or use drag-and-drop.</p>
|
||||
<p>Drag-and-drop the items in the tree in order to change their
|
||||
order. See <a href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> for more
|
||||
interaction options, including using keyboard shortcuts.</p>
|
||||
order. See <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> for
|
||||
more interaction options, including using keyboard shortcuts.</p>
|
||||
<h2>Customizing the launcher</h2>
|
||||
<ul>
|
||||
<li>The icon of a launcher can be changed just like a normal note. See
|
||||
<a
|
||||
href="#root/_help_p9kXRFAkwN4o">Note Icons</a> for more information.</li>
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_p9kXRFAkwN4o">Note Icons</a> for more information.</li>
|
||||
<li>The title of the launcher can also be changed.</li>
|
||||
</ul>
|
||||
<h3>Resetting</h3>
|
||||
@ -54,42 +70,39 @@
|
||||
<p>Right click either the <em>Available launchers</em> or <em>Visible launchers</em> sections
|
||||
and select one of the options:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>Note Launcher</strong>
|
||||
<br>A note launcher will simply navigate to a specified note.</p>
|
||||
<li><strong>Note Launcher</strong>
|
||||
<br>A note launcher will simply navigate to a specified note.
|
||||
<ol>
|
||||
<li>Set the <code spellcheck="false">target</code> promoted attribute to the
|
||||
note to navigate to.</li>
|
||||
<li>Optionally, set <code spellcheck="false">hoistedNote</code> to hoist a particular
|
||||
note. See <a href="#root/_help_OR8WJ7Iz9K4U">Note Hoisting</a> for
|
||||
note. See <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_OR8WJ7Iz9K4U">Note Hoisting</a> for
|
||||
more information.</li>
|
||||
<li>Optionally, set a <code spellcheck="false">keyboardShortcut</code> to trigger
|
||||
the launcher.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Script Launcher</strong>
|
||||
<br>An advanced launcher which will run a script upon pressing. See
|
||||
<a
|
||||
href="#root/_help_CdNpE2pqjmI6">Scripts</a> for more information.</p>
|
||||
<ol>
|
||||
<li>Set <code spellcheck="false">script</code> to point to the desired script
|
||||
to run.</li>
|
||||
<li>Optionally, set a <code spellcheck="false">keyboardShortcut</code> to trigger
|
||||
the launcher.</li>
|
||||
</ol>
|
||||
<li><strong>Script Launcher</strong>
|
||||
<br>An advanced launcher which will run a script upon pressing. See
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">Scripting</a> for more information.
|
||||
<ol>
|
||||
<li>Set <code spellcheck="false">script</code> to point to the desired script
|
||||
to run.</li>
|
||||
<li>Optionally, set a <code spellcheck="false">keyboardShortcut</code> to trigger
|
||||
the launcher.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
<li class="ck-list-marker-bold">
|
||||
<p><strong>Custom Widget</strong>
|
||||
</p>
|
||||
<p>Allows defining a custom widget to be rendered inside the launcher. See
|
||||
<a
|
||||
href="#root/_help_SynTBQiBsdYJ">Widget Basics</a> for more information.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Spacers</strong>
|
||||
<br>Launchers that create some distance between other launchers for better
|
||||
visual distinction.</p>
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/MgibgPcfeuGz/_help_SynTBQiBsdYJ">Widget Basics</a> for more information.</p>
|
||||
</li>
|
||||
<li><strong>Spacers</strong>
|
||||
<br>Launchers that create some distance between other launchers for better
|
||||
visual distinction.</li>
|
||||
</ol>
|
||||
<p>Launchers are configured via predefined <a href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>
|
||||
<p>Launchers are configured via predefined <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>
|
||||
@ -4,8 +4,7 @@ class="image image-style-align-center">
|
||||
<img style="aspect-ratio:1398/1015;" src="Split View_2_Split View_im.png"
|
||||
width="1398" height="1015">
|
||||
</figure>
|
||||
|
||||
<h2><strong>Interactions</strong></h2>
|
||||
<h2><strong>Interactions</strong></h2>
|
||||
<ul>
|
||||
<li>Press the
|
||||
<img src="Split View_Split View_imag.png">button to the right of a note's title to open a new split to the right
|
||||
@ -51,11 +50,12 @@ class="image image-style-align-center">
|
||||
<ul>
|
||||
<li>On smartphones, the split views are laid out vertically (one on the top
|
||||
and one on the bottom), instead of horizontally as on the desktop.</li>
|
||||
<li>There can be only one split open per tab.</li>
|
||||
<li>It's not possible to resize the two split panes.</li>
|
||||
<li>When the keyboard is opened, the active note will be “maximized”, thus
|
||||
allowing for more space even when a split is open. When the keyboard is
|
||||
closed, the splits become equal in size again.</li>
|
||||
<li
|
||||
>There can be only one split open per tab.</li>
|
||||
<li>It's not possible to resize the two split panes.</li>
|
||||
<li>When the keyboard is opened, the active note will be “maximized”, thus
|
||||
allowing for more space even when a split is open. When the keyboard is
|
||||
closed, the splits become equal in size again.</li>
|
||||
</ul>
|
||||
<p>Interaction:</p>
|
||||
<ul>
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
<ul>
|
||||
<li>For the vertical layout, the tabs will be placed at the top but to the
|
||||
right of the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
|
||||
<li>For the horizontal layout, the tabs will be placed at the top in full-width,
|
||||
<li
|
||||
>For the horizontal layout, the tabs will be placed at the top in full-width,
|
||||
above the <a href="#root/_help_oPVyFC7WL2Lp">note tree</a>, allowing for
|
||||
more tabs to be comfortably displayed.</li>
|
||||
</ul>
|
||||
@ -22,7 +23,8 @@
|
||||
href="#root/_help_luNhaphA37EO">Split View</a>. Each tab can have one or more
|
||||
notes, displayed horizontally.</li>
|
||||
<li>Tabs can be reordered by drag-and-dropping it into a new position.</li>
|
||||
<li>An existing tab can be displayed in a new window by dragging the tab upwards
|
||||
<li
|
||||
>An existing tab can be displayed in a new window by dragging the tab upwards
|
||||
or downwards. It is not possible to combine tabs back into another window.</li>
|
||||
</ul>
|
||||
<h2>Keyboard interaction</h2>
|
||||
@ -36,7 +38,29 @@
|
||||
<li><kbd>Ctrl</kbd>+<kbd>Tab</kbd> and <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd> to
|
||||
go to the next or previous tab.</li>
|
||||
<li><kbd>Ctrl</kbd>+<kbd>1</kbd>, <kbd>Ctrl</kbd>+<kbd>2</kbd>, up to <kbd>Ctrl</kbd>+<kbd>9</kbd> to
|
||||
activate the first, second and up til ninth tab.</li>
|
||||
activate the first, second and up to ninth tab.</li>
|
||||
<li>There is also a shortcut to go to the last tab, but it is not assigned
|
||||
a key by default.</li>
|
||||
</ul>
|
||||
</ul>
|
||||
<h2>Mobile</h2>
|
||||
<figure class="image image-style-align-right image_resized" style="width:34.12%;">
|
||||
<img style="aspect-ratio:1242/2688;" src="Tabs_IMG_1767.PNG"
|
||||
width="1242" height="2688">
|
||||
</figure>
|
||||
<p>Tabs are also supported on the <a class="reference-link" href="#root/pOsGYCXsbNQG/Otzi9La2YAUX/_help_RDslemsQ6gCp">Mobile Frontend</a>.</p>
|
||||
<p>Since v0.102.0, the tabs are displayed by pressing the dedicated tab switcher
|
||||
button in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>.
|
||||
In this view the tabs are laid out on a grid with a preview of the note
|
||||
content.</p>
|
||||
<p>The context menu button at the top-right of the popup allows creating
|
||||
a new tab, reopening the last closed tab and closing all the tabs.</p>
|
||||
<p><a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_luNhaphA37EO">Split View</a>s
|
||||
are also indicated in the tab switcher, with two titles displayed in a
|
||||
tab.</p>
|
||||
<aside class="admonition note">
|
||||
<p>Versions prior to v0.102.0 also supported tabs, but they were displayed
|
||||
directly above the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>.
|
||||
The decision to use a more mobile-like tab switcher was taken because the
|
||||
original tab bar could not support many tabs at once and the new design
|
||||
better aligns with how mobile applications handle tabs.</p>
|
||||
</aside>
|
||||
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Tabs_IMG_1767.PNG
generated
vendored
Normal file
|
After Width: | Height: | Size: 195 KiB |
@ -1,35 +1,168 @@
|
||||
<p>Trilium (<a href="#root/_help_WOcw2SLH6tbX">server edition</a>) has a mobile
|
||||
web frontend which is optimized for touch based devices - smartphones and
|
||||
tablets. It is activated automatically during login process based on browser
|
||||
detection.</p>
|
||||
<p>Mobile frontend is limited in features compared to full desktop frontend.
|
||||
<figure class="image image_resized image-style-align-right" style="width:33.52%;">
|
||||
<img style="aspect-ratio:1242/2688;" src="Mobile Frontend_IMG_1765.PNG"
|
||||
width="1242" height="2688">
|
||||
</figure>
|
||||
<p>Trilium has a mobile web frontend which is optimized for touch based devices
|
||||
- smartphones and tablets. It is activated automatically during login process
|
||||
based on browser detection.</p>
|
||||
<p>Mobile frontend is limited in features compared to the full desktop version.
|
||||
See below for more details on this.</p>
|
||||
<p>Note that this is not an Android/iOS app, this is just mobile friendly
|
||||
web page served on the <a href="#root/_help_WOcw2SLH6tbX">server edition</a>.</p>
|
||||
<h2>Layout basics</h2>
|
||||
<p>Unlike the desktop version, the mobile version has a slightly different
|
||||
UI meant to better fit the constrained screens of a mobile phone.</p>
|
||||
<p>Here is a non-exhaustive list of differences between the desktop version
|
||||
and the mobile one:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> is
|
||||
displayed as a sidebar. To display the sidebar, press the button in the
|
||||
top-left of the screen.</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>There is also a swipe gesture that can be done from the left of the screen,
|
||||
but the browser's navigation gesture interferes with it most of the time
|
||||
(depending on the platform).</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Press and hold a note to display the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/oPVyFC7WL2Lp/_help_YtSN43OrfzaA">Note tree contextual menu</a>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>The <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_Ms1nauBra7gq">Quick search</a> bar
|
||||
is also displayed at the top of the note tree.</li>
|
||||
<li>The full <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a> function
|
||||
can be triggered either from either the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a> or
|
||||
from the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>,
|
||||
if configured.</li>
|
||||
<li>
|
||||
<p>The <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a> is
|
||||
displayed at the bottom of the screen.</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>The launch bar uses a different configuration for icons than the desktop
|
||||
version. See the dedicated page for more information on how to configure
|
||||
it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Most of the note-related actions are grouped in the horizontal dots icon
|
||||
on the top-right of the note.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_3seOhtN8uLIY">Tabs</a> are
|
||||
grouped under a tab switcher in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>,
|
||||
where the tabs are displayed in a full-screen grid with preview for easy
|
||||
switching, as well as additional options such as reopening closed tabs.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Since v0.100.0, <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_luNhaphA37EO">Split View</a> can
|
||||
also be used in mobile view, but with a maximum of two panes at once. The
|
||||
splits are displayed vertically instead of horizontally.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Starting with v0.102.0, the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a> is
|
||||
enforced on mobile. This brings features such as the note badges, note
|
||||
type switcher or collection properties which would otherwise not be available.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Installing as a PWA</h2>
|
||||
<p>The mobile view can be set up as a PWA. While this does not offer any
|
||||
offline capabilities, it will display the application in full-screen and
|
||||
makes it easy to access via your mobile phone's home screen.</p>
|
||||
<h3>On iOS with Safari</h3>
|
||||
<ol>
|
||||
<li>Open your default web browser and access your Trilium instance.</li>
|
||||
<li
|
||||
>Login.</li>
|
||||
<li>
|
||||
<p>Press the […] button in the bottom-right of the screen and select Share.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Scroll down to reveal the full list of items and choose “Add to Home Screen”.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Press “Add” and the web app will be available.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>On Android with Google Chrome</h3>
|
||||
<aside class="admonition important">
|
||||
<p>Google Chrome requires the server to be served over HTTPS in order to
|
||||
display in full-screen. If using HTTP, the app will appear like a normal
|
||||
web page (similar to a bookmark).</p>
|
||||
</aside>
|
||||
<ol>
|
||||
<li>Open your default web browser and access your Trilium instance.</li>
|
||||
<li
|
||||
>Login.</li>
|
||||
<li>
|
||||
<p>Press the three vertical dots icon in the top-right of the screen and
|
||||
select <em>Add to Home screen.</em>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Select the <em>Install</em> option.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Select an appropriate name.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The web app will appear as an application, not on the home screen.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>On Android with Brave</h3>
|
||||
<aside class="admonition important">
|
||||
<p>Brave requires the server to be served over HTTPS in order to display
|
||||
in full-screen. If using HTTP, the app will appear like a normal web page
|
||||
(similar to a bookmark).</p>
|
||||
</aside>
|
||||
<ol>
|
||||
<li>Open your default web browser and access your Trilium instance.</li>
|
||||
<li
|
||||
>Login.</li>
|
||||
<li>
|
||||
<p>Press the three vertical dots icon in the bottom-right of the screen and
|
||||
select <em>Add to Home screen</em>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Press the <em>Install</em> option.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The web app will appear as an application, not on the home screen.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>On Samsung Browser</h3>
|
||||
<ol>
|
||||
<li>Open your default web browser and access your Trilium instance.</li>
|
||||
<li
|
||||
>Login.</li>
|
||||
<li>
|
||||
<p>Press the hamburger menu in the bottom-right of the screen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Select <em>Add to</em>, followed by <em>Home screen</em>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Press <em>Add</em> and the web app will appear on the home page.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h2>Testing via the desktop application</h2>
|
||||
<p>If you are running Trilium without a dedicated <a href="#root/_help_WOcw2SLH6tbX">server installation</a>,
|
||||
you can still test the mobile application using the desktop application.
|
||||
For more information, see <a class="reference-link" href="#root/_help_nRqcgfTb97uV">Using the desktop application as a server</a>.
|
||||
To access it go to <code spellcheck="false">http://<ip>:37840/login?mobile</code> .</p>
|
||||
<h2>Limitations</h2>
|
||||
<p>Mobile frontend provides only some of the features of the full desktop
|
||||
frontend:</p>
|
||||
<ul>
|
||||
<li>it is possible to browse the whole note tree, read and edit all types
|
||||
of notes, but you can create only text notes</li>
|
||||
<li>reading and editing <a href="#root/_help_bwg0e8ewQMak">protected notes</a> is
|
||||
possible, but creating them is not supported</li>
|
||||
<li>editing options is not supported</li>
|
||||
<li>cloning notes is not supported</li>
|
||||
<li>uploading file attachments is not supported</li>
|
||||
</ul>
|
||||
<h2>Forcing mobile/desktop frontend</h2>
|
||||
<p>Trilium decides automatically whether to use mobile or desktop frontend.
|
||||
<p>Trilium decides automatically whether to use mobile or desktop front-end.
|
||||
If this is not appropriate, you can use <code spellcheck="false">?mobile</code> or
|
||||
<code
|
||||
spellcheck="false">?desktop</code>query param on <strong>login</strong> page (Note: you might
|
||||
need to log out).</p>
|
||||
<p>Alternatively, simply select <em>Switch to Mobile/Desktop Version</em> in
|
||||
the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a>.</p>
|
||||
<h2>Scripting</h2>
|
||||
<p>You can alter the behavior with <a href="#root/_help_CdNpE2pqjmI6">scripts</a> just
|
||||
like for normal frontend. For script notes to be executed, they need to
|
||||
have labeled <code spellcheck="false">#run=mobileStartup</code>.</p>
|
||||
<p>You can alter the behavior with <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">Scripting</a>,
|
||||
just like for normal frontend. For script notes to be executed, they need
|
||||
to have labeled <code spellcheck="false">#run=mobileStartup</code>.</p>
|
||||
<p>Custom <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a> widgets
|
||||
are also supported.</p>
|
||||
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Mobile Frontend_IMG_1765.PNG
generated
vendored
Normal file
|
After Width: | Height: | Size: 280 KiB |
@ -3,7 +3,7 @@
|
||||
import dateUtils from "../date_utils.js";
|
||||
import path from "path";
|
||||
import packageInfo from "../../../package.json" with { type: "json" };
|
||||
import { getContentDisposition } from "../utils.js";
|
||||
import { getContentDisposition, waitForStreamToFinish } from "../utils.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import sanitize from "sanitize-filename";
|
||||
import fs from "fs";
|
||||
@ -468,6 +468,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
|
||||
taskContext.taskSucceeded(null);
|
||||
}
|
||||
|
||||
|
||||
async function exportToZipFile(noteId: string, format: ExportFormat, zipFilePath: string, zipExportOptions?: AdvancedExportOptions) {
|
||||
const fileOutputStream = fs.createWriteStream(zipFilePath);
|
||||
const taskContext = new TaskContext("no-progress-reporting", "export", null);
|
||||
@ -479,6 +480,7 @@ async function exportToZipFile(noteId: string, format: ExportFormat, zipFilePath
|
||||
}
|
||||
|
||||
await exportToZip(taskContext, note.getParentBranches()[0], format, fileOutputStream, false, zipExportOptions);
|
||||
await waitForStreamToFinish(fileOutputStream);
|
||||
|
||||
log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
|
||||
}
|
||||
|
||||
@ -40,18 +40,24 @@ export default function buildLaunchBarConfig() {
|
||||
type: "launcher",
|
||||
command: "showRecentChanges",
|
||||
icon: "bx bx-history"
|
||||
}
|
||||
},
|
||||
searchNotes: {
|
||||
title: t("hidden-subtree.search-notes-title"),
|
||||
type: "launcher",
|
||||
command: "searchNotes",
|
||||
icon: "bx bx-search",
|
||||
},
|
||||
bookmarks: {
|
||||
title: t("hidden-subtree.bookmarks-title"),
|
||||
type: "launcher",
|
||||
builtinWidget: "bookmarks",
|
||||
icon: "bx bx-bookmark"
|
||||
},
|
||||
};
|
||||
|
||||
const desktopAvailableLaunchers: HiddenSubtreeItem[] = [
|
||||
{
|
||||
id: "_lbBackInHistory",
|
||||
...sharedLaunchers.backInHistory
|
||||
},
|
||||
{
|
||||
id: "_lbForwardInHistory",
|
||||
...sharedLaunchers.forwardInHistory
|
||||
},
|
||||
{ id: "_lbBackInHistory", ...sharedLaunchers.backInHistory },
|
||||
{ id: "_lbForwardInHistory", ...sharedLaunchers.forwardInHistory },
|
||||
{
|
||||
id: "_commandPalette",
|
||||
title: t("hidden-subtree.command-palette"),
|
||||
@ -82,11 +88,7 @@ export default function buildLaunchBarConfig() {
|
||||
},
|
||||
{
|
||||
id: "_lbSearch",
|
||||
title: t("hidden-subtree.search-notes-title"),
|
||||
type: "launcher",
|
||||
command: "searchNotes",
|
||||
icon: "bx bx-search",
|
||||
attributes: [{ type: "label", name: "desktopOnly" }]
|
||||
...sharedLaunchers.searchNotes
|
||||
},
|
||||
{
|
||||
id: "_lbJumpTo",
|
||||
@ -128,13 +130,7 @@ export default function buildLaunchBarConfig() {
|
||||
baseSize: "50",
|
||||
growthFactor: "0"
|
||||
},
|
||||
{
|
||||
id: "_lbBookmarks",
|
||||
title: t("hidden-subtree.bookmarks-title"),
|
||||
type: "launcher",
|
||||
builtinWidget: "bookmarks",
|
||||
icon: "bx bx-bookmark"
|
||||
},
|
||||
{ id: "_lbBookmarks", ...sharedLaunchers.bookmarks },
|
||||
{
|
||||
id: "_lbToday",
|
||||
...sharedLaunchers.openToday
|
||||
@ -179,22 +175,15 @@ export default function buildLaunchBarConfig() {
|
||||
|
||||
const mobileAvailableLaunchers: HiddenSubtreeItem[] = [
|
||||
{ id: "_lbMobileNewNote", ...sharedLaunchers.newNote },
|
||||
{ id: "_lbMobileSearchNotes", ...sharedLaunchers.searchNotes },
|
||||
{ id: "_lbMobileToday", ...sharedLaunchers.openToday },
|
||||
{
|
||||
id: "_lbMobileRecentChanges",
|
||||
...sharedLaunchers.recentChanges
|
||||
}
|
||||
{ id: "_lbMobileRecentChanges", ...sharedLaunchers.recentChanges },
|
||||
{ id: "_lbMobileBookmarks", ...sharedLaunchers.bookmarks }
|
||||
];
|
||||
|
||||
const mobileVisibleLaunchers: HiddenSubtreeItem[] = [
|
||||
{
|
||||
id: "_lbMobileBackInHistory",
|
||||
...sharedLaunchers.backInHistory
|
||||
},
|
||||
{
|
||||
id: "_lbMobileForwardInHistory",
|
||||
...sharedLaunchers.forwardInHistory
|
||||
},
|
||||
{ id: "_lbMobileBackInHistory", ...sharedLaunchers.backInHistory },
|
||||
{ id: "_lbMobileForwardInHistory", ...sharedLaunchers.forwardInHistory },
|
||||
{
|
||||
id: "_lbMobileJumpTo",
|
||||
title: t("hidden-subtree.jump-to-note-title"),
|
||||
@ -210,7 +199,8 @@ export default function buildLaunchBarConfig() {
|
||||
id: "_lbMobileTabSwitcher",
|
||||
title: t("hidden-subtree.tab-switcher-title"),
|
||||
type: "launcher",
|
||||
builtinWidget: "mobileTabSwitcher"
|
||||
builtinWidget: "mobileTabSwitcher",
|
||||
icon: "bx bx-rectangle"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
import chardet from "chardet";
|
||||
import crypto from "crypto";
|
||||
import escape from "escape-html";
|
||||
@ -516,6 +515,13 @@ function slugify(text: string) {
|
||||
.replace(/(^-|-$)+/g, ""); // trim dashes
|
||||
}
|
||||
|
||||
export function waitForStreamToFinish(stream: any): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on("finish", () => resolve());
|
||||
stream.on("error", (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
compareVersions,
|
||||
constantTimeCompare,
|
||||
@ -556,5 +562,6 @@ export default {
|
||||
toBase64,
|
||||
toMap,
|
||||
toObject,
|
||||
unescapeHtml
|
||||
unescapeHtml,
|
||||
waitForStreamToFinish
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Documentation
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/y2NjgAW23yyj/Documentation_image.png" width="205" height="162">
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/qIpo9UiTZNdm/Documentation_image.png" width="205" height="162">
|
||||
|
||||
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
|
||||
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.
|
||||
|
||||
248
docs/User Guide/!!!meta.json
vendored
@ -1491,20 +1491,6 @@
|
||||
"isInheritable": false,
|
||||
"position": 20
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "bwg0e8ewQMak",
|
||||
"isInheritable": false,
|
||||
"position": 30
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "CdNpE2pqjmI6",
|
||||
"isInheritable": false,
|
||||
"position": 40
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"name": "shareAlias",
|
||||
@ -1518,11 +1504,90 @@
|
||||
"value": "bx bx-mobile-alt",
|
||||
"isInheritable": false,
|
||||
"position": 60
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "x3i7MxGccDuM",
|
||||
"isInheritable": false,
|
||||
"position": 70
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "IjZS7iK5EXtb",
|
||||
"isInheritable": false,
|
||||
"position": 80
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oPVyFC7WL2Lp",
|
||||
"isInheritable": false,
|
||||
"position": 90
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "YtSN43OrfzaA",
|
||||
"isInheritable": false,
|
||||
"position": 110
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "xYmIYSP6wE3F",
|
||||
"isInheritable": false,
|
||||
"position": 120
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "Ms1nauBra7gq",
|
||||
"isInheritable": false,
|
||||
"position": 130
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "eIg8jdvaoNNd",
|
||||
"isInheritable": false,
|
||||
"position": 140
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "3seOhtN8uLIY",
|
||||
"isInheritable": false,
|
||||
"position": 150
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "CdNpE2pqjmI6",
|
||||
"isInheritable": false,
|
||||
"position": 160
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "luNhaphA37EO",
|
||||
"isInheritable": false,
|
||||
"position": 170
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Mobile Frontend.md",
|
||||
"attachments": []
|
||||
"attachments": [
|
||||
{
|
||||
"attachmentId": "JRliU5bOLszn",
|
||||
"title": "IMG_1765.PNG",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "Mobile Frontend_IMG_1765.PNG"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
@ -2793,6 +2858,20 @@
|
||||
"value": "tabs",
|
||||
"isInheritable": false,
|
||||
"position": 40
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "RDslemsQ6gCp",
|
||||
"isInheritable": false,
|
||||
"position": 50
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "xYmIYSP6wE3F",
|
||||
"isInheritable": false,
|
||||
"position": 60
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
@ -2821,6 +2900,14 @@
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "2_Tabs_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "rFdKVhoVtsLA",
|
||||
"title": "IMG_1767.PNG",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "Tabs_IMG_1767.PNG"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -2840,20 +2927,6 @@
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "x0JgW8UqGXvq",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "x3i7MxGccDuM",
|
||||
"isInheritable": false,
|
||||
"position": 20
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
@ -2861,41 +2934,6 @@
|
||||
"isInheritable": false,
|
||||
"position": 30
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "p9kXRFAkwN4o",
|
||||
"isInheritable": false,
|
||||
"position": 40
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "OR8WJ7Iz9K4U",
|
||||
"isInheritable": false,
|
||||
"position": 50
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "CdNpE2pqjmI6",
|
||||
"isInheritable": false,
|
||||
"position": 60
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "SynTBQiBsdYJ",
|
||||
"isInheritable": false,
|
||||
"position": 70
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "OFXdgB2nNk1F",
|
||||
"isInheritable": false,
|
||||
"position": 80
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"name": "iconClass",
|
||||
@ -2909,6 +2947,55 @@
|
||||
"value": "launch-bar",
|
||||
"isInheritable": false,
|
||||
"position": 90
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "OFXdgB2nNk1F",
|
||||
"isInheritable": false,
|
||||
"position": 100
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "SynTBQiBsdYJ",
|
||||
"isInheritable": false,
|
||||
"position": 110
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "CdNpE2pqjmI6",
|
||||
"isInheritable": false,
|
||||
"position": 120
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "OR8WJ7Iz9K4U",
|
||||
"isInheritable": false,
|
||||
"position": 130
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "p9kXRFAkwN4o",
|
||||
"isInheritable": false,
|
||||
"position": 140
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "x3i7MxGccDuM",
|
||||
"isInheritable": false,
|
||||
"position": 150
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "x0JgW8UqGXvq",
|
||||
"isInheritable": false,
|
||||
"position": 160
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
@ -5256,28 +5343,25 @@
|
||||
"value": "bx bx-bookmarks",
|
||||
"isInheritable": false,
|
||||
"position": 30
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "xYmIYSP6wE3F",
|
||||
"isInheritable": false,
|
||||
"position": 40
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "x3i7MxGccDuM",
|
||||
"isInheritable": false,
|
||||
"position": 50
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Bookmarks.md",
|
||||
"attachments": [
|
||||
{
|
||||
"attachmentId": "99dD0P74W8QJ",
|
||||
"title": "bookmark-folder.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "Bookmarks_bookmark-folder.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "9dX71aLlSl9R",
|
||||
"title": "bookmarks.gif",
|
||||
"role": "image",
|
||||
"mime": "image/gif",
|
||||
"position": 10,
|
||||
"dataFileName": "Bookmarks_bookmarks.gif"
|
||||
}
|
||||
]
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
# Bookmarks
|
||||
To easily access selected notes, you can bookmark them. See demo:
|
||||
Frequently used notes can be bookmarked, which will make them appear in the <a class="reference-link" href="../UI%20Elements/Launch%20Bar.md">Launch Bar</a> for easy access.
|
||||
|
||||

|
||||
## Configuring the launch bar
|
||||
|
||||
If bookmarks don't appear in the launch bar, then most likely the bookmark section has been hidden. Go to the <a class="reference-link" href="../UI%20Elements/Launch%20Bar.md">Launch Bar</a> configuration from the <a class="reference-link" href="../UI%20Elements/Global%20menu.md">Global menu</a> and ensure _Bookmarks_ is in the _Visible Launchers_ section.
|
||||
|
||||
## Bookmark folder
|
||||
|
||||
Space in the left panel is limited, and you might want to bookmark many items. One possible solution is to bookmark a folder, so it shows its children:
|
||||
|
||||

|
||||
To do this, bookmark a folder and assign it the `#bookmarkFolder` label.
|
||||
|
||||
To do this, you need to add a `#bookmarkFolder` label to the note.
|
||||
## Mobile
|
||||
|
||||
On mobile, bookmarks are only displayed starting with v0.102.0. Because of the more constrained screen size, the bookmarks are grouped under a single icon instead of displaying them as separate icons.
|
||||
|
||||
When pressed, a menu will appear listing all the bookmarks. Bookmark folders are also supported and will appear as sub-menus.
|
||||
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 305 KiB |
@ -1,7 +1,11 @@
|
||||
# Launch Bar
|
||||
## Position of the Launch bar
|
||||
|
||||
Depending on the layout selected, the launcher bar will either be on the left side of the screen with buttons displayed vertically or at the top of the screen. See [Vertical and horizontal layout](Vertical%20and%20horizontal%20layout.md) for more information.
|
||||
On desktop, depending on the layout selected, the launcher bar will either be on the left side of the screen with buttons displayed vertically or at the top of the screen. See <a class="reference-link" href="Vertical%20and%20horizontal%20layout.md">Vertical and horizontal layout</a> for more information.
|
||||
|
||||
On mobile, the launch bar will always be at the bottom.
|
||||
|
||||
If there are too many items in the launch bar to fit the screen, it will become scrollable.
|
||||
|
||||
## Terminology
|
||||
|
||||
@ -9,30 +13,38 @@ Depending on the layout selected, the launcher bar will either be on the left si
|
||||
* **Available Launcher**: a launcher that is not displayed on the launch bar, but can be added.
|
||||
* **Visible Launcher**: a launcher that is currently displayed on the launch bar.
|
||||
|
||||
## Configuring the Launch bar
|
||||
## Configuring the desktop Launch bar
|
||||
|
||||
There are two ways to configure the launch bar:
|
||||
|
||||
* Right click in the empty space between launchers on the launch bar and select _Configure Launchbar._
|
||||
* Click on the [Global menu](Global%20menu.md) and select _Configure Launchbar_.
|
||||
* Click on the <a class="reference-link" href="Global%20menu.md">Global menu</a> and select _Configure Launchbar_.
|
||||
|
||||
This will open a new tab with the [Note Tree](Note%20Tree.md) listing the launchers.
|
||||
This will open a new tab with the <a class="reference-link" href="Note%20Tree.md">Note Tree</a> listing the launchers.
|
||||
|
||||

|
||||
|
||||
Expanding _Available Launchers_ section will show the list of launchers that are not displayed on the launch bar. The _Visible Launchers_ will show the ones that are currently displayed.
|
||||
|
||||
## Configuring the mobile launch bar
|
||||
|
||||
The launch bar on mobile uses a different configuration from the desktop one. The reasoning is that not all desktop icons are available on mobile, and fewer icons fit on a mobile screen.
|
||||
|
||||
To configure the launch bar on mobile, go to <a class="reference-link" href="Global%20menu.md">Global menu</a> and select _Configure Launchbar_.
|
||||
|
||||
The configure the mobile launch bar while on the desktop (especially useful to configure more complicated launchers such as scripts or custom widgets), go to <a class="reference-link" href="Global%20menu.md">Global menu</a> → Advanced → Show Hidden Subtree and look for the _Mobile Launch Bar_ section. While in the hidden subtree, it's also possible to drag launchers between the _Mobile Launch Bar_ and (Desktop) _Launch Bar_ sections.
|
||||
|
||||
### Adding/removing and reordering launchers
|
||||
|
||||
To display a new launcher in the launch bar, first look for it in the _Available Launchers_ section. Then right click it and select _Move to visible launchers_. It is also possible to drag and drop the item manually.
|
||||
|
||||
Similarly, to remove it from the launch bar, simply look for it in _Visible Launchers_ then right click it and select _Move to available launchers_ or use drag-and-drop.
|
||||
|
||||
Drag-and-drop the items in the tree in order to change their order. See [Note Tree](Note%20Tree.md) for more interaction options, including using keyboard shortcuts.
|
||||
Drag-and-drop the items in the tree in order to change their order. See <a class="reference-link" href="Note%20Tree.md">Note Tree</a> for more interaction options, including using keyboard shortcuts.
|
||||
|
||||
## Customizing the launcher
|
||||
|
||||
* The icon of a launcher can be changed just like a normal note. See [Note Icons](../Notes/Note%20Icons.md) for more information.
|
||||
* The icon of a launcher can be changed just like a normal note. See <a class="reference-link" href="../Notes/Note%20Icons.md">Note Icons</a> for more information.
|
||||
* The title of the launcher can also be changed.
|
||||
|
||||
### Resetting
|
||||
@ -45,19 +57,17 @@ Right click either the _Available launchers_ or _Visible launchers_ sections and
|
||||
|
||||
1. **Note Launcher**
|
||||
A note launcher will simply navigate to a specified note.
|
||||
|
||||
1. Set the `target` promoted attribute to the note to navigate to.
|
||||
2. Optionally, set `hoistedNote` to hoist a particular note. See [Note Hoisting](../Navigation/Note%20Hoisting.md) for more information.
|
||||
2. Optionally, set `hoistedNote` to hoist a particular note. See <a class="reference-link" href="../Navigation/Note%20Hoisting.md">Note Hoisting</a> for more information.
|
||||
3. Optionally, set a `keyboardShortcut` to trigger the launcher.
|
||||
2. **Script Launcher**
|
||||
An advanced launcher which will run a script upon pressing. See [Scripts](../../Scripting.md) for more information.
|
||||
|
||||
An advanced launcher which will run a script upon pressing. See <a class="reference-link" href="../../Scripting.md">Scripting</a> for more information.
|
||||
1. Set `script` to point to the desired script to run.
|
||||
2. Optionally, set a `keyboardShortcut` to trigger the launcher.
|
||||
3. **Custom Widget**
|
||||
|
||||
Allows defining a custom widget to be rendered inside the launcher. See [Widget Basics](../../Scripting/Frontend%20Basics/Custom%20Widgets/Widget%20Basics.md) for more information.
|
||||
Allows defining a custom widget to be rendered inside the launcher. See <a class="reference-link" href="../../Scripting/Frontend%20Basics/Custom%20Widgets/Widget%20Basics.md">Widget Basics</a> for more information.
|
||||
4. **Spacers**
|
||||
Launchers that create some distance between other launchers for better visual distinction.
|
||||
|
||||
Launchers are configured via predefined [Promoted Attributes](../../Advanced%20Usage/Attributes/Promoted%20Attributes.md).
|
||||
Launchers are configured via predefined <a class="reference-link" href="../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a>.
|
||||
@ -26,5 +26,20 @@ Since tabs are a commonly used feature, there are multiple keyboard shortcuts th
|
||||
* <kbd>Ctrl</kbd>+<kbd>W</kbd> to close the current tab.
|
||||
* <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd> to reopen the last closed tab.
|
||||
* <kbd>Ctrl</kbd>+<kbd>Tab</kbd> and <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd> to go to the next or previous tab.
|
||||
* <kbd>Ctrl</kbd>+<kbd>1</kbd>, <kbd>Ctrl</kbd>+<kbd>2</kbd>, up to <kbd>Ctrl</kbd>+<kbd>9</kbd> to activate the first, second and up til ninth tab.
|
||||
* There is also a shortcut to go to the last tab, but it is not assigned a key by default.
|
||||
* <kbd>Ctrl</kbd>+<kbd>1</kbd>, <kbd>Ctrl</kbd>+<kbd>2</kbd>, up to <kbd>Ctrl</kbd>+<kbd>9</kbd> to activate the first, second and up to ninth tab.
|
||||
* There is also a shortcut to go to the last tab, but it is not assigned a key by default.
|
||||
|
||||
## Mobile
|
||||
|
||||
<figure class="image image-style-align-right image_resized" style="width:34.12%;"><img style="aspect-ratio:1242/2688;" src="Tabs_IMG_1767.PNG" width="1242" height="2688"></figure>
|
||||
|
||||
Tabs are also supported on the <a class="reference-link" href="../../Installation%20%26%20Setup/Mobile%20Frontend.md">Mobile Frontend</a>.
|
||||
|
||||
Since v0.102.0, the tabs are displayed by pressing the dedicated tab switcher button in the <a class="reference-link" href="Launch%20Bar.md">Launch Bar</a>. In this view the tabs are laid out on a grid with a preview of the note content.
|
||||
|
||||
The context menu button at the top-right of the popup allows creating a new tab, reopening the last closed tab and closing all the tabs.
|
||||
|
||||
<a class="reference-link" href="Split%20View.md">Split View</a>s are also indicated in the tab switcher, with two titles displayed in a tab.
|
||||
|
||||
> [!NOTE]
|
||||
> Versions prior to v0.102.0 also supported tabs, but they were displayed directly above the <a class="reference-link" href="Launch%20Bar.md">Launch Bar</a>. The decision to use a more mobile-like tab switcher was taken because the original tab bar could not support many tabs at once and the new design better aligns with how mobile applications handle tabs.
|
||||
BIN
docs/User Guide/User Guide/Basic Concepts and Features/UI Elements/Tabs_IMG_1767.PNG
vendored
Normal file
|
After Width: | Height: | Size: 195 KiB |
@ -1,28 +1,85 @@
|
||||
# Mobile Frontend
|
||||
Trilium ([server edition](Server%20Installation.md)) has a mobile web frontend which is optimized for touch based devices - smartphones and tablets. It is activated automatically during login process based on browser detection.
|
||||
<figure class="image image_resized image-style-align-right" style="width:33.52%;"><img style="aspect-ratio:1242/2688;" src="Mobile Frontend_IMG_1765.PNG" width="1242" height="2688"></figure>
|
||||
|
||||
Mobile frontend is limited in features compared to full desktop frontend. See below for more details on this.
|
||||
Trilium has a mobile web frontend which is optimized for touch based devices - smartphones and tablets. It is activated automatically during login process based on browser detection.
|
||||
|
||||
Note that this is not an Android/iOS app, this is just mobile friendly web page served on the [server edition](Server%20Installation.md).
|
||||
Mobile frontend is limited in features compared to the full desktop version. See below for more details on this.
|
||||
|
||||
## Layout basics
|
||||
|
||||
Unlike the desktop version, the mobile version has a slightly different UI meant to better fit the constrained screens of a mobile phone.
|
||||
|
||||
Here is a non-exhaustive list of differences between the desktop version and the mobile one:
|
||||
|
||||
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a> is displayed as a sidebar. To display the sidebar, press the button in the top-left of the screen.
|
||||
|
||||
* There is also a swipe gesture that can be done from the left of the screen, but the browser's navigation gesture interferes with it most of the time (depending on the platform).
|
||||
* Press and hold a note to display the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree/Note%20tree%20contextual%20menu.md">Note tree contextual menu</a>.
|
||||
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Navigation/Quick%20search.md">Quick search</a> bar is also displayed at the top of the note tree.
|
||||
* The full <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Navigation/Search.md">Search</a> function can be triggered either from either the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Global%20menu.md">Global menu</a> or from the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a>, if configured.
|
||||
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a> is displayed at the bottom of the screen.
|
||||
|
||||
* The launch bar uses a different configuration for icons than the desktop version. See the dedicated page for more information on how to configure it.
|
||||
* Most of the note-related actions are grouped in the horizontal dots icon on the top-right of the note.
|
||||
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Tabs.md">Tabs</a> are grouped under a tab switcher in the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a>, where the tabs are displayed in a full-screen grid with preview for easy switching, as well as additional options such as reopening closed tabs.
|
||||
* Since v0.100.0, <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Split%20View.md">Split View</a> can also be used in mobile view, but with a maximum of two panes at once. The splits are displayed vertically instead of horizontally.
|
||||
* Starting with v0.102.0, the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/New%20Layout.md">New Layout</a> is enforced on mobile. This brings features such as the note badges, note type switcher or collection properties which would otherwise not be available.
|
||||
|
||||
## Installing as a PWA
|
||||
|
||||
The mobile view can be set up as a PWA. While this does not offer any offline capabilities, it will display the application in full-screen and makes it easy to access via your mobile phone's home screen.
|
||||
|
||||
### On iOS with Safari
|
||||
|
||||
1. Open your default web browser and access your Trilium instance.
|
||||
2. Login.
|
||||
3. Press the \[…\] button in the bottom-right of the screen and select Share.
|
||||
4. Scroll down to reveal the full list of items and choose “Add to Home Screen”.
|
||||
5. Press “Add” and the web app will be available.
|
||||
|
||||
### On Android with Google Chrome
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Google Chrome requires the server to be served over HTTPS in order to display in full-screen. If using HTTP, the app will appear like a normal web page (similar to a bookmark).
|
||||
|
||||
1. Open your default web browser and access your Trilium instance.
|
||||
2. Login.
|
||||
3. Press the three vertical dots icon in the top-right of the screen and select _Add to Home screen._
|
||||
4. Select the _Install_ option.
|
||||
5. Select an appropriate name.
|
||||
6. The web app will appear as an application, not on the home screen.
|
||||
|
||||
### On Android with Brave
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Brave requires the server to be served over HTTPS in order to display in full-screen. If using HTTP, the app will appear like a normal web page (similar to a bookmark).
|
||||
|
||||
1. Open your default web browser and access your Trilium instance.
|
||||
2. Login.
|
||||
3. Press the three vertical dots icon in the bottom-right of the screen and select _Add to Home screen_.
|
||||
4. Press the _Install_ option.
|
||||
5. The web app will appear as an application, not on the home screen.
|
||||
|
||||
### On Samsung Browser
|
||||
|
||||
1. Open your default web browser and access your Trilium instance.
|
||||
2. Login.
|
||||
3. Press the hamburger menu in the bottom-right of the screen.
|
||||
4. Select _Add to_, followed by _Home screen_.
|
||||
5. Press _Add_ and the web app will appear on the home page.
|
||||
|
||||
## Testing via the desktop application
|
||||
|
||||
If you are running Trilium without a dedicated [server installation](Server%20Installation.md), you can still test the mobile application using the desktop application. For more information, see <a class="reference-link" href="Desktop%20Installation/Using%20the%20desktop%20application%20.md">Using the desktop application as a server</a>. To access it go to `http://<ip>:37840/login?mobile` .
|
||||
|
||||
## Limitations
|
||||
|
||||
Mobile frontend provides only some of the features of the full desktop frontend:
|
||||
|
||||
* it is possible to browse the whole note tree, read and edit all types of notes, but you can create only text notes
|
||||
* reading and editing [protected notes](../Basic%20Concepts%20and%20Features/Notes/Protected%20Notes.md) is possible, but creating them is not supported
|
||||
* editing options is not supported
|
||||
* cloning notes is not supported
|
||||
* uploading file attachments is not supported
|
||||
|
||||
## Forcing mobile/desktop frontend
|
||||
|
||||
Trilium decides automatically whether to use mobile or desktop frontend. If this is not appropriate, you can use `?mobile` or `?desktop` query param on **login** page (Note: you might need to log out).
|
||||
Trilium decides automatically whether to use mobile or desktop front-end. If this is not appropriate, you can use `?mobile` or `?desktop` query param on **login** page (Note: you might need to log out).
|
||||
|
||||
Alternatively, simply select _Switch to Mobile/Desktop Version_ in the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Global%20menu.md">Global menu</a>.
|
||||
|
||||
## Scripting
|
||||
|
||||
You can alter the behavior with [scripts](../Scripting.md) just like for normal frontend. For script notes to be executed, they need to have labeled `#run=mobileStartup`.
|
||||
You can alter the behavior with <a class="reference-link" href="../Scripting.md">Scripting</a>, just like for normal frontend. For script notes to be executed, they need to have labeled `#run=mobileStartup`.
|
||||
|
||||
Custom <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a> widgets are also supported.
|
||||
BIN
docs/User Guide/User Guide/Installation & Setup/Mobile Frontend_IMG_1765.PNG
vendored
Normal file
|
After Width: | Height: | Size: 280 KiB |
42
flake.nix
@ -113,7 +113,7 @@
|
||||
[
|
||||
moreutils # sponge
|
||||
nodejs.python
|
||||
removeReferencesTo
|
||||
removeReferencesTo
|
||||
]
|
||||
++ lib.optionals (app == "desktop" || app == "edit-docs") [
|
||||
copyDesktopItems
|
||||
@ -126,7 +126,7 @@
|
||||
which
|
||||
electron
|
||||
]
|
||||
++ lib.optionals (app == "server") [
|
||||
++ lib.optionals (app == "server" || app == "build-docs") [
|
||||
makeBinaryWrapper
|
||||
]
|
||||
++ lib.optionals stdenv.hostPlatform.isDarwin [
|
||||
@ -153,7 +153,7 @@
|
||||
|
||||
# This file is a symlink into /build which is not allowed.
|
||||
postFixup = ''
|
||||
rm $out/opt/trilium*/node_modules/better-sqlite3/node_modules/.bin/prebuild-install || true
|
||||
find $out/opt -name prebuild-install -path "*/better-sqlite3/node_modules/.bin/*" -delete || true
|
||||
'';
|
||||
|
||||
components = [
|
||||
@ -169,6 +169,7 @@
|
||||
"packages/highlightjs"
|
||||
"packages/turndown-plugin-gfm"
|
||||
|
||||
"apps/build-docs"
|
||||
"apps/client"
|
||||
"apps/db-compare"
|
||||
"apps/desktop"
|
||||
@ -277,11 +278,46 @@
|
||||
'';
|
||||
};
|
||||
|
||||
build-docs = makeApp {
|
||||
app = "build-docs";
|
||||
preBuildCommands = ''
|
||||
pushd apps/server
|
||||
pnpm rebuild || true
|
||||
popd
|
||||
'';
|
||||
buildTask = "client:build && pnpm run server:build && pnpm run --filter build-docs build";
|
||||
mainProgram = "trilium-build-docs";
|
||||
installCommands = ''
|
||||
mkdir -p $out/{bin,opt/trilium-build-docs}
|
||||
|
||||
# Copy build-docs dist
|
||||
cp --archive apps/build-docs/dist/* $out/opt/trilium-build-docs
|
||||
|
||||
# Copy server dist (needed for runtime)
|
||||
mkdir -p $out/opt/trilium-build-docs/server
|
||||
cp --archive apps/server/dist/* $out/opt/trilium-build-docs/server/
|
||||
|
||||
# Copy client dist (needed for runtime)
|
||||
mkdir -p $out/opt/trilium-build-docs/client
|
||||
cp --archive apps/client/dist/* $out/opt/trilium-build-docs/client/
|
||||
|
||||
# Copy share-theme (needed for exports)
|
||||
mkdir -p $out/opt/trilium-build-docs/packages/share-theme
|
||||
cp --archive packages/share-theme/dist/* $out/opt/trilium-build-docs/packages/share-theme/
|
||||
|
||||
# Create wrapper script
|
||||
makeWrapper ${lib.getExe nodejs} $out/bin/trilium-build-docs \
|
||||
--add-flags $out/opt/trilium-build-docs/cli.cjs \
|
||||
--set TRILIUM_RESOURCE_DIR $out/opt/trilium-build-docs/server
|
||||
'';
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
packages.desktop = desktop;
|
||||
packages.server = server;
|
||||
packages.edit-docs = edit-docs;
|
||||
packages.build-docs = build-docs;
|
||||
|
||||
packages.default = desktop;
|
||||
|
||||
|
||||
6
pnpm-lock.yaml
generated
@ -164,6 +164,9 @@ importers:
|
||||
fs-extra:
|
||||
specifier: 11.3.3
|
||||
version: 11.3.3
|
||||
js-yaml:
|
||||
specifier: 4.1.1
|
||||
version: 4.1.1
|
||||
react:
|
||||
specifier: 19.2.4
|
||||
version: 19.2.4
|
||||
@ -16534,8 +16537,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-markdown-gfm@47.4.0':
|
||||
dependencies:
|
||||
@ -16946,6 +16947,7 @@ snapshots:
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@ckeditor/ckeditor5-utils@47.4.0':
|
||||
|
||||