mirror of
https://github.com/zadam/trilium.git
synced 2025-11-03 11:56:01 +01:00
Compare commits
6 Commits
electron_n
...
feat/push-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f31f9c78 | ||
|
|
22c5651eeb | ||
|
|
68a10a9813 | ||
|
|
b204964e57 | ||
|
|
b2c869d7ab | ||
|
|
307e17f9c8 |
@@ -1,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{js,ts,tsx}]
|
||||
[*.{js,ts,.tsx}]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
|
||||
5
.github/actions/build-electron/action.yml
vendored
5
.github/actions/build-electron/action.yml
vendored
@@ -74,7 +74,7 @@ runs:
|
||||
|
||||
- name: Update build info
|
||||
shell: ${{ inputs.shell }}
|
||||
run: pnpm run chore:update-build-info
|
||||
run: npm run chore:update-build-info
|
||||
|
||||
# Critical debugging configuration
|
||||
- name: Run electron-forge build with enhanced logging
|
||||
@@ -86,8 +86,7 @@ runs:
|
||||
APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
|
||||
WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }}
|
||||
TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
|
||||
TARGET_ARCH: ${{ inputs.arch }}
|
||||
run: pnpm run --filter desktop electron-forge:make --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
|
||||
run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
|
||||
|
||||
# Add DMG signing step
|
||||
- name: Sign DMG
|
||||
|
||||
6
.github/actions/build-server/action.yml
vendored
6
.github/actions/build-server/action.yml
vendored
@@ -10,9 +10,9 @@ runs:
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
@@ -23,7 +23,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run chore:update-build-info
|
||||
pnpm run --filter server package
|
||||
pnpm nx --project=server package
|
||||
- name: Prepare artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
name: "Deploy to Cloudflare Pages"
|
||||
description: "Deploys to Cloudflare Pages on either a temporary branch with preview comment, or on the production version if on the main branch."
|
||||
inputs:
|
||||
project_name:
|
||||
description: "CloudFlare Pages project name"
|
||||
comment_body:
|
||||
description: "The message to display when deployment is ready"
|
||||
default: "Deployment is ready."
|
||||
required: false
|
||||
production_url:
|
||||
description: "The URL to mention as the production URL."
|
||||
required: true
|
||||
deploy_dir:
|
||||
description: "The directory from which to deploy."
|
||||
required: true
|
||||
cloudflare_api_token:
|
||||
description: "The Cloudflare API token to use for deployment."
|
||||
required: true
|
||||
cloudflare_account_id:
|
||||
description: "The Cloudflare account ID to use for deployment."
|
||||
required: true
|
||||
github_token:
|
||||
description: "The GitHub token to use for posting PR comments."
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# Install wrangler globally to avoid workspace issues
|
||||
- name: Install Wrangler
|
||||
shell: bash
|
||||
run: npm install -g wrangler
|
||||
|
||||
# Deploy using Wrangler (use pre-installed wrangler)
|
||||
- name: Deploy to Cloudflare Pages
|
||||
id: deploy
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ inputs.cloudflare_api_token }}
|
||||
accountId: ${{ inputs.cloudflare_account_id }}
|
||||
command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=${{ github.ref_name }}
|
||||
wranglerVersion: '' # Use pre-installed version
|
||||
|
||||
# Deploy preview for PRs
|
||||
- name: Deploy Preview to Cloudflare Pages
|
||||
id: preview-deployment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ inputs.cloudflare_api_token }}
|
||||
accountId: ${{ inputs.cloudflare_account_id }}
|
||||
command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=pr-${{ github.event.pull_request.number }}
|
||||
wranglerVersion: '' # Use pre-installed version
|
||||
|
||||
# Post deployment URL as PR comment
|
||||
- name: Comment PR with Preview URL
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
COMMENT_BODY: ${{ inputs.comment_body }}
|
||||
PRODUCTION_URL: ${{ inputs.production_url }}
|
||||
PROJECT_NAME: ${{ inputs.project_name }}
|
||||
with:
|
||||
github-token: ${{ inputs.github_token }}
|
||||
script: |
|
||||
const prNumber = context.issue.number;
|
||||
// Construct preview URL based on Cloudflare Pages pattern
|
||||
const projectName = process.env.PROJECT_NAME;
|
||||
const previewUrl = `https://pr-${prNumber}.${projectName}.pages.dev`;
|
||||
|
||||
// Check if we already commented
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const customMessage = process.env.COMMENT_BODY;
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes(customMessage)
|
||||
);
|
||||
|
||||
const mainUrl = process.env.PRODUCTION_URL;
|
||||
const commentBody = `${customMessage}!\n\n🔗 Preview URL: ${previewUrl}\n📖 Production URL: ${mainUrl}\n\n✅ All checks passed\n\n_This preview will be updated automatically with new commits._`;
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: prNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
40
.github/instructions/nx.instructions.md
vendored
Normal file
40
.github/instructions/nx.instructions.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
// This file is automatically generated by Nx Console
|
||||
|
||||
You are in an nx workspace using Nx 21.3.9 and pnpm as the package manager.
|
||||
|
||||
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
||||
|
||||
# General Guidelines
|
||||
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
|
||||
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
|
||||
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
|
||||
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
|
||||
|
||||
# Generation Guidelines
|
||||
If the user wants to generate something, use the following flow:
|
||||
|
||||
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
|
||||
- get the available generators using the 'nx_generators' tool
|
||||
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
|
||||
- get generator details using the 'nx_generator_schema' tool
|
||||
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
|
||||
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
|
||||
- open the generator UI using the 'nx_open_generate_ui' tool
|
||||
- wait for the user to finish the generator
|
||||
- read the generator log file using the 'nx_read_generator_log' tool
|
||||
- use the information provided in the log file to answer the user's question or continue with what they were doing
|
||||
|
||||
# Running Tasks Guidelines
|
||||
If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow:
|
||||
- Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed).
|
||||
- If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command
|
||||
- Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
|
||||
- If the user would like to rerun the task or command, always use `nx run <taskId>` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed
|
||||
- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output.
|
||||
|
||||
|
||||
|
||||
453
.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
vendored
Normal file
453
.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
vendored
Normal file
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const;
|
||||
const README_PATTERN = /^README(?:[-.](.+))?\.md$/;
|
||||
|
||||
interface SyncConfig {
|
||||
mainRepoPath: string;
|
||||
wikiPath: string;
|
||||
docsPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown to GitHub Wiki format
|
||||
* - Images:  → [[image.png]]
|
||||
* - Links: [text](page.md) → [[text|page]]
|
||||
*/
|
||||
async function convertToWikiFormat(wikiDir: string): Promise<void> {
|
||||
console.log('Converting to GitHub Wiki format...');
|
||||
const mdFiles = await findFiles(wikiDir, ['.md']);
|
||||
let convertedCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
let content = await fs.readFile(file, 'utf-8');
|
||||
const originalContent = content;
|
||||
|
||||
// Convert image references to wiki format
|
||||
//  → [[image.png]]
|
||||
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||
// Skip external URLs
|
||||
if (src.startsWith('http://') || src.startsWith('https://')) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding
|
||||
let imagePath = src;
|
||||
if (src.includes('%')) {
|
||||
try {
|
||||
imagePath = decodeURIComponent(src);
|
||||
} catch {
|
||||
imagePath = src;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract just the filename for wiki syntax
|
||||
const filename = path.basename(imagePath);
|
||||
|
||||
// Use wiki syntax for images
|
||||
// If alt text exists, add it after pipe
|
||||
if (alt && alt.trim()) {
|
||||
return `[[${filename}|alt=${alt}]]`;
|
||||
} else {
|
||||
return `[[${filename}]]`;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert internal markdown links to wiki format
|
||||
// [text](../path/to/Page.md) → [[text|Page]]
|
||||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
||||
// Skip external URLs, anchors, and images
|
||||
if (href.startsWith('http://') ||
|
||||
href.startsWith('https://') ||
|
||||
href.startsWith('#') ||
|
||||
href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Check if it's a markdown file link
|
||||
if (href.endsWith('.md') || href.includes('.md#')) {
|
||||
// Decode URL encoding
|
||||
let decodedHref = href;
|
||||
if (href.includes('%')) {
|
||||
try {
|
||||
decodedHref = decodeURIComponent(href);
|
||||
} catch {
|
||||
decodedHref = href;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract page name without extension and path
|
||||
let pageName = decodedHref
|
||||
.replace(/\.md(#.*)?$/, '') // Remove .md and anchor
|
||||
.split('/') // Split by path
|
||||
.pop() || ''; // Get last part (filename)
|
||||
|
||||
// Convert spaces to hyphens (GitHub wiki convention)
|
||||
pageName = pageName.replace(/ /g, '-');
|
||||
|
||||
// Use wiki link syntax
|
||||
if (text === pageName || text === pageName.replace(/-/g, ' ')) {
|
||||
return `[[${pageName}]]`;
|
||||
} else {
|
||||
return `[[${text}|${pageName}]]`;
|
||||
}
|
||||
}
|
||||
|
||||
// For other internal links, just decode URL encoding
|
||||
if (href.includes('%') && !href.startsWith('http')) {
|
||||
try {
|
||||
const decodedHref = decodeURIComponent(href);
|
||||
return `[${text}](${decodedHref})`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Save if modified
|
||||
if (content !== originalContent) {
|
||||
await fs.writeFile(file, content, 'utf-8');
|
||||
const relativePath = path.relative(wikiDir, file);
|
||||
console.log(` Converted: ${relativePath}`);
|
||||
convertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (convertedCount > 0) {
|
||||
console.log(`Converted ${convertedCount} files to wiki format`);
|
||||
} else {
|
||||
console.log('No files needed conversion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all files matching the given extensions
|
||||
*/
|
||||
async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a directory recursively
|
||||
*/
|
||||
async function getAllFiles(dir: string): Promise<Set<string>> {
|
||||
const files = new Set<string>();
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const relativePath = path.relative(dir, fullPath);
|
||||
|
||||
// Skip .git directory
|
||||
if (entry.name === '.git' || relativePath.startsWith('.git')) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.add(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory might not exist yet
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten directory structure - move all files to root
|
||||
* GitHub Wiki prefers flat structure
|
||||
*/
|
||||
async function flattenStructure(wikiDir: string): Promise<void> {
|
||||
console.log('Flattening directory structure for wiki...');
|
||||
const allFiles = await getAllFiles(wikiDir);
|
||||
let movedCount = 0;
|
||||
|
||||
for (const file of allFiles) {
|
||||
// Skip if already at root
|
||||
if (!file.includes('/')) continue;
|
||||
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
const basename = path.basename(file);
|
||||
|
||||
// Create unique name if file already exists at root
|
||||
let newName = basename;
|
||||
let counter = 1;
|
||||
while (await fileExists(path.join(wikiDir, newName))) {
|
||||
const ext = path.extname(basename);
|
||||
const nameWithoutExt = basename.slice(0, -ext.length);
|
||||
newName = `${nameWithoutExt}-${counter}${ext}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
|
||||
// Move file to root
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Moved: ${file} → ${newName}`);
|
||||
movedCount++;
|
||||
}
|
||||
|
||||
if (movedCount > 0) {
|
||||
console.log(`Moved ${movedCount} files to root`);
|
||||
|
||||
// Clean up empty directories
|
||||
await cleanEmptyDirectories(wikiDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty directories recursively
|
||||
*/
|
||||
async function cleanEmptyDirectories(dir: string): Promise<void> {
|
||||
const allDirs = await getAllDirectories(dir);
|
||||
|
||||
for (const subDir of allDirs) {
|
||||
try {
|
||||
const entries = await fs.readdir(subDir);
|
||||
if (entries.length === 0 || (entries.length === 1 && entries[0] === '.git')) {
|
||||
await fs.rmdir(subDir);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all directories recursively
|
||||
*/
|
||||
async function getAllDirectories(dir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== '.git') {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
dirs.push(fullPath);
|
||||
await walk(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync files from source to wiki
|
||||
*/
|
||||
async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> {
|
||||
console.log('Syncing files to wiki...');
|
||||
|
||||
// Get all valid source files
|
||||
const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS);
|
||||
const sourceRelativePaths = new Set<string>();
|
||||
|
||||
// Copy all source files
|
||||
console.log(`Found ${sourceFiles.length} files to sync`);
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const relativePath = path.relative(sourceDir, file);
|
||||
sourceRelativePaths.add(relativePath);
|
||||
|
||||
const targetPath = path.join(wikiDir, relativePath);
|
||||
const targetDir = path.dirname(targetPath);
|
||||
|
||||
// Create directory structure
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Copy file
|
||||
await fs.copyFile(file, targetPath);
|
||||
}
|
||||
|
||||
// Remove orphaned files
|
||||
const wikiFiles = await getAllFiles(wikiDir);
|
||||
for (const wikiFile of wikiFiles) {
|
||||
if (!sourceRelativePaths.has(wikiFile) && !wikiFile.startsWith('Home')) {
|
||||
const fullPath = path.join(wikiDir, wikiFile);
|
||||
await fs.unlink(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy root README.md to wiki as Home.md if it exists
|
||||
*/
|
||||
async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> {
|
||||
const rootReadmePath = path.join(mainRepoPath, 'README.md');
|
||||
const wikiHomePath = path.join(wikiPath, 'Home.md');
|
||||
|
||||
try {
|
||||
await fs.access(rootReadmePath);
|
||||
await fs.copyFile(rootReadmePath, wikiHomePath);
|
||||
console.log(' Copied root README.md as Home.md');
|
||||
} catch (error) {
|
||||
console.log(' No root README.md found to use as Home page');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename README files to wiki-compatible names
|
||||
*/
|
||||
async function renameReadmeFiles(wikiDir: string): Promise<void> {
|
||||
console.log('Converting README files for wiki compatibility...');
|
||||
const files = await fs.readdir(wikiDir);
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.match(README_PATTERN);
|
||||
if (match) {
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
let newName: string;
|
||||
|
||||
if (match[1]) {
|
||||
// Language-specific README
|
||||
newName = `Home-${match[1]}.md`;
|
||||
} else {
|
||||
// Main README
|
||||
newName = 'Home.md';
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Renamed: ${file} → ${newName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any changes in the wiki
|
||||
*/
|
||||
async function hasChanges(wikiDir: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir });
|
||||
return stdout.trim().length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking git status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration from environment variables
|
||||
*/
|
||||
function getConfig(): SyncConfig {
|
||||
const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo';
|
||||
const wikiPath = process.env.WIKI_PATH || 'wiki';
|
||||
const docsPath = path.join(mainRepoPath, 'docs');
|
||||
|
||||
return { mainRepoPath, wikiPath, docsPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sync function
|
||||
*/
|
||||
async function syncDocsToWiki(): Promise<void> {
|
||||
const config = getConfig();
|
||||
const flattenWiki = process.env.FLATTEN_WIKI === 'true';
|
||||
|
||||
console.log('Starting documentation sync to wiki...');
|
||||
console.log(`Source: ${config.docsPath}`);
|
||||
console.log(`Target: ${config.wikiPath}`);
|
||||
console.log(`Flatten structure: ${flattenWiki}`);
|
||||
|
||||
try {
|
||||
// Verify paths exist
|
||||
await fs.access(config.docsPath);
|
||||
await fs.access(config.wikiPath);
|
||||
|
||||
// Sync files
|
||||
await syncFiles(config.docsPath, config.wikiPath);
|
||||
|
||||
// Copy root README.md as Home.md
|
||||
await copyRootReadme(config.mainRepoPath, config.wikiPath);
|
||||
|
||||
// Convert to wiki format
|
||||
await convertToWikiFormat(config.wikiPath);
|
||||
|
||||
// Optionally flatten directory structure
|
||||
if (flattenWiki) {
|
||||
await flattenStructure(config.wikiPath);
|
||||
}
|
||||
|
||||
// Rename README files to wiki-compatible names
|
||||
await renameReadmeFiles(config.wikiPath);
|
||||
|
||||
// Check for changes
|
||||
const changed = await hasChanges(config.wikiPath);
|
||||
|
||||
if (changed) {
|
||||
console.log('\nChanges detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::true\n');
|
||||
} else {
|
||||
console.log('\nNo changes detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::false\n');
|
||||
}
|
||||
|
||||
console.log('Sync completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
syncDocsToWiki();
|
||||
}
|
||||
|
||||
export { syncDocsToWiki };
|
||||
437
.github/scripts/sync-docs-to-wiki.ts
vendored
Executable file
437
.github/scripts/sync-docs-to-wiki.ts
vendored
Executable file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const;
|
||||
const README_PATTERN = /^README(?:[-.](.+))?\.md$/;
|
||||
|
||||
interface SyncConfig {
|
||||
mainRepoPath: string;
|
||||
wikiPath: string;
|
||||
docsPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all files matching the given extensions
|
||||
*/
|
||||
async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a directory recursively
|
||||
*/
|
||||
async function getAllFiles(dir: string): Promise<Set<string>> {
|
||||
const files = new Set<string>();
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const relativePath = path.relative(dir, fullPath);
|
||||
|
||||
// Skip .git directory
|
||||
if (entry.name === '.git' || relativePath.startsWith('.git')) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.add(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory might not exist yet
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync files from source to wiki, preserving directory structure and removing orphaned files
|
||||
*/
|
||||
async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> {
|
||||
console.log('Analyzing files to sync...');
|
||||
|
||||
// Get all valid source files
|
||||
const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS);
|
||||
const sourceRelativePaths = new Set<string>();
|
||||
|
||||
// Copy all source files and track their paths
|
||||
console.log(`Found ${sourceFiles.length} files to sync`);
|
||||
let copiedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const relativePath = path.relative(sourceDir, file);
|
||||
sourceRelativePaths.add(relativePath);
|
||||
|
||||
const targetPath = path.join(wikiDir, relativePath);
|
||||
const targetDir = path.dirname(targetPath);
|
||||
|
||||
// Create directory structure
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Check if file needs updating (compare modification times)
|
||||
let needsCopy = true;
|
||||
try {
|
||||
const sourceStat = await fs.stat(file);
|
||||
const targetStat = await fs.stat(targetPath);
|
||||
// Only copy if source is newer or sizes differ
|
||||
needsCopy = sourceStat.mtime > targetStat.mtime || sourceStat.size !== targetStat.size;
|
||||
} catch {
|
||||
// Target doesn't exist, needs copy
|
||||
needsCopy = true;
|
||||
}
|
||||
|
||||
if (needsCopy) {
|
||||
await fs.copyFile(file, targetPath);
|
||||
console.log(` Updated: ${relativePath}`);
|
||||
copiedCount++;
|
||||
} else {
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Updated ${copiedCount} files, ${skippedCount} unchanged`);
|
||||
|
||||
// Find and remove files that don't exist in source
|
||||
console.log('Checking for orphaned files in wiki...');
|
||||
const wikiFiles = await getAllFiles(wikiDir);
|
||||
let removedCount = 0;
|
||||
|
||||
for (const wikiFile of wikiFiles) {
|
||||
// Check if this file should exist (either as-is or will be renamed)
|
||||
let shouldExist = sourceRelativePaths.has(wikiFile);
|
||||
|
||||
// Special handling for Home files that will be created from READMEs
|
||||
if (wikiFile.startsWith('Home')) {
|
||||
const readmeVariant1 = wikiFile.replace(/^Home(-.*)?\.md$/, 'README$1.md');
|
||||
const readmeVariant2 = wikiFile.replace(/^Home-(.+)\.md$/, 'README.$1.md');
|
||||
shouldExist = sourceRelativePaths.has(readmeVariant1) || sourceRelativePaths.has(readmeVariant2) || sourceRelativePaths.has('README.md');
|
||||
}
|
||||
|
||||
if (!shouldExist) {
|
||||
const fullPath = path.join(wikiDir, wikiFile);
|
||||
await fs.unlink(fullPath);
|
||||
console.log(` Removed: ${wikiFile}`);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(`Removed ${removedCount} orphaned files`);
|
||||
|
||||
// Clean up empty directories
|
||||
await cleanEmptyDirectories(wikiDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty directories recursively
|
||||
*/
|
||||
async function cleanEmptyDirectories(dir: string): Promise<void> {
|
||||
async function removeEmptyDirs(currentDir: string): Promise<boolean> {
|
||||
if (currentDir === dir) return false; // Don't remove root
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
// Skip .git directory
|
||||
const filteredEntries = entries.filter(e => e.name !== '.git');
|
||||
|
||||
if (filteredEntries.length === 0) {
|
||||
await fs.rmdir(currentDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check subdirectories
|
||||
for (const entry of filteredEntries) {
|
||||
if (entry.isDirectory()) {
|
||||
const subDir = path.join(currentDir, entry.name);
|
||||
await removeEmptyDirs(subDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Check again after cleaning subdirectories
|
||||
const remainingEntries = await fs.readdir(currentDir);
|
||||
const filteredRemaining = remainingEntries.filter(e => e !== '.git');
|
||||
|
||||
if (filteredRemaining.length === 0 && currentDir !== dir) {
|
||||
await fs.rmdir(currentDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all directories and process them
|
||||
const allDirs = await getAllDirectories(dir);
|
||||
for (const subDir of allDirs) {
|
||||
await removeEmptyDirs(subDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all directories recursively
|
||||
*/
|
||||
async function getAllDirectories(dir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== '.git') {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
dirs.push(fullPath);
|
||||
await walk(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix references in markdown files for wiki compatibility
|
||||
*
|
||||
* Issues fixed:
|
||||
* 1. URL-encoded image references (spaces as %20) need to match actual filenames
|
||||
* 2. Internal markdown links need to be converted to wiki syntax
|
||||
* 3. Images can optionally use wiki syntax [[image.png]] for better compatibility
|
||||
*/
|
||||
async function fixImageReferences(wikiDir: string): Promise<void> {
|
||||
console.log('Fixing references for GitHub Wiki compatibility...');
|
||||
const mdFiles = await findFiles(wikiDir, ['.md']);
|
||||
let fixedCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
let content = await fs.readFile(file, 'utf-8');
|
||||
let modified = false;
|
||||
const originalContent = content;
|
||||
|
||||
// Step 1: Fix URL-encoded image references
|
||||
// Convert  to 
|
||||
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||
// Skip external URLs
|
||||
if (src.startsWith('http://') || src.startsWith('https://')) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding if present
|
||||
if (src.includes('%')) {
|
||||
try {
|
||||
const decodedSrc = decodeURIComponent(src);
|
||||
return ``;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Step 2: Fix internal links - decode URL encoding but keep standard markdown format
|
||||
// GitHub Wiki actually supports standard markdown links with relative paths
|
||||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
||||
// Skip external URLs, anchors, and images
|
||||
if (href.startsWith('http://') ||
|
||||
href.startsWith('https://') ||
|
||||
href.startsWith('#') ||
|
||||
href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding for all internal links
|
||||
if (href.includes('%')) {
|
||||
try {
|
||||
const decodedHref = decodeURIComponent(href);
|
||||
return `[${text}](${decodedHref})`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Check if content was modified
|
||||
if (content !== originalContent) {
|
||||
modified = true;
|
||||
await fs.writeFile(file, content, 'utf-8');
|
||||
const relativePath = path.relative(wikiDir, file);
|
||||
console.log(` Fixed references in: ${relativePath}`);
|
||||
fixedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedCount > 0) {
|
||||
console.log(`Fixed references in ${fixedCount} files`);
|
||||
} else {
|
||||
console.log('No references needed fixing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename README files to wiki-compatible names
|
||||
*/
|
||||
async function renameReadmeFiles(wikiDir: string): Promise<void> {
|
||||
console.log('Converting README files for wiki compatibility...');
|
||||
const files = await fs.readdir(wikiDir);
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.match(README_PATTERN);
|
||||
if (match) {
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
let newName: string;
|
||||
|
||||
if (match[1]) {
|
||||
// Language-specific README (e.g., README-ZH_CN.md or README.es.md)
|
||||
newName = `Home-${match[1]}.md`;
|
||||
} else {
|
||||
// Main README
|
||||
newName = 'Home.md';
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Renamed: ${file} → ${newName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any changes in the wiki
|
||||
*/
|
||||
async function hasChanges(wikiDir: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir });
|
||||
return stdout.trim().length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking git status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy root README.md to wiki as Home.md if it exists
|
||||
*/
|
||||
async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> {
|
||||
const rootReadmePath = path.join(mainRepoPath, 'README.md');
|
||||
const wikiHomePath = path.join(wikiPath, 'Home.md');
|
||||
|
||||
try {
|
||||
await fs.access(rootReadmePath);
|
||||
await fs.copyFile(rootReadmePath, wikiHomePath);
|
||||
console.log(' Copied root README.md as Home.md');
|
||||
} catch (error) {
|
||||
// Root README doesn't exist or can't be accessed
|
||||
console.log(' No root README.md found to use as Home page');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration from environment variables
|
||||
*/
|
||||
function getConfig(): SyncConfig {
|
||||
const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo';
|
||||
const wikiPath = process.env.WIKI_PATH || 'wiki';
|
||||
const docsPath = path.join(mainRepoPath, 'docs');
|
||||
|
||||
return { mainRepoPath, wikiPath, docsPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sync function
|
||||
*/
|
||||
async function syncDocsToWiki(): Promise<void> {
|
||||
const config = getConfig();
|
||||
|
||||
console.log('Starting documentation sync to wiki...');
|
||||
console.log(`Source: ${config.docsPath}`);
|
||||
console.log(`Target: ${config.wikiPath}`);
|
||||
|
||||
try {
|
||||
// Verify paths exist
|
||||
await fs.access(config.docsPath);
|
||||
await fs.access(config.wikiPath);
|
||||
|
||||
// Sync files (copy new/updated, remove orphaned)
|
||||
await syncFiles(config.docsPath, config.wikiPath);
|
||||
|
||||
// Copy root README.md as Home.md
|
||||
await copyRootReadme(config.mainRepoPath, config.wikiPath);
|
||||
|
||||
// Fix image and link references for wiki compatibility
|
||||
await fixImageReferences(config.wikiPath);
|
||||
|
||||
// Rename README files to wiki-compatible names
|
||||
await renameReadmeFiles(config.wikiPath);
|
||||
|
||||
// Check for changes
|
||||
const changed = await hasChanges(config.wikiPath);
|
||||
|
||||
if (changed) {
|
||||
console.log('\nChanges detected in wiki');
|
||||
// GitHub Actions output format
|
||||
process.stdout.write('::set-output name=changes::true\n');
|
||||
} else {
|
||||
console.log('\nNo changes detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::false\n');
|
||||
}
|
||||
|
||||
console.log('Sync completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
syncDocsToWiki();
|
||||
}
|
||||
|
||||
export { syncDocsToWiki };
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -95,6 +95,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
74
.github/workflows/deploy-docs.yml
vendored
74
.github/workflows/deploy-docs.yml
vendored
@@ -1,74 +0,0 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
# Trigger on push to main branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master # Also support master branch
|
||||
# Only run when docs files change
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'apps/edit-docs/**'
|
||||
- 'packages/share-theme/**'
|
||||
|
||||
# Allow manual triggering from Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Run on pull requests for preview deployments
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'apps/edit-docs/**'
|
||||
- 'packages/share-theme/**'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: Build and Deploy Documentation
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
# Required permissions for deployment
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write # For PR preview comments
|
||||
id-token: write # For OIDC authentication (if needed)
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Trigger build of documentation
|
||||
run: pnpm docs:build
|
||||
|
||||
- name: Validate Built Site
|
||||
run: |
|
||||
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
|
||||
|
||||
- name: Deploy
|
||||
uses: ./.github/actions/deploy-to-cloudflare-pages
|
||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||
with:
|
||||
project_name: "trilium-docs"
|
||||
comment_body: "📚 Documentation preview is ready"
|
||||
production_url: "https://docs.triliumnotes.org"
|
||||
deploy_dir: "site"
|
||||
cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
39
.github/workflows/dev.yml
vendored
39
.github/workflows/dev.yml
vendored
@@ -19,24 +19,45 @@ permissions:
|
||||
pull-requests: write # for PR comments
|
||||
|
||||
jobs:
|
||||
check-affected:
|
||||
name: Check affected jobs (NX)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # needed for https://github.com/marketplace/actions/nx-set-shas
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
- name: Check affected
|
||||
run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build
|
||||
|
||||
test_dev:
|
||||
name: Test development
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-affected
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Run the unit tests
|
||||
run: pnpm run test:all
|
||||
|
||||
@@ -45,6 +66,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test_dev
|
||||
- check-affected
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
@@ -53,7 +75,7 @@ jobs:
|
||||
- name: Update build info
|
||||
run: pnpm run chore:update-build-info
|
||||
- name: Trigger client build
|
||||
run: pnpm client:build
|
||||
run: pnpm nx run client:build
|
||||
- name: Send client bundle stats to RelativeCI
|
||||
if: false
|
||||
uses: relative-ci/agent-action@v3
|
||||
@@ -61,7 +83,7 @@ jobs:
|
||||
webpackStatsFile: ./apps/client/dist/webpack-stats.json
|
||||
key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }}
|
||||
- name: Trigger server build
|
||||
run: pnpm run server:build
|
||||
run: pnpm nx run server:build
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
@@ -73,6 +95,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_docker
|
||||
- check-affected
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -89,7 +112,7 @@ jobs:
|
||||
- name: Update build info
|
||||
run: pnpm run chore:update-build-info
|
||||
- name: Trigger build
|
||||
run: pnpm server:build
|
||||
run: pnpm nx run server:build
|
||||
|
||||
- name: Set IMAGE_NAME to lowercase
|
||||
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
||||
|
||||
30
.github/workflows/main-docker.yml
vendored
30
.github/workflows/main-docker.yml
vendored
@@ -44,9 +44,9 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install npm dependencies
|
||||
@@ -82,16 +82,16 @@ jobs:
|
||||
require-healthy: true
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm --filter=server-e2e e2e
|
||||
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e
|
||||
|
||||
- name: Upload Playwright trace
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Playwright trace (${{ matrix.dockerfile }})
|
||||
path: test-output/playwright/output
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: Playwright report (${{ matrix.dockerfile }})
|
||||
@@ -116,10 +116,10 @@ jobs:
|
||||
- dockerfile: Dockerfile
|
||||
platform: linux/arm64
|
||||
image: ubuntu-24.04-arm
|
||||
- dockerfile: Dockerfile.legacy
|
||||
- dockerfile: Dockerfile
|
||||
platform: linux/arm/v7
|
||||
image: ubuntu-24.04-arm
|
||||
- dockerfile: Dockerfile.legacy
|
||||
- dockerfile: Dockerfile
|
||||
platform: linux/arm/v8
|
||||
image: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.image }}
|
||||
@@ -144,20 +144,20 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Update build info
|
||||
run: pnpm run chore:update-build-info
|
||||
|
||||
- name: Run the TypeScript build
|
||||
run: pnpm run server:build
|
||||
|
||||
- name: Update build info
|
||||
run: pnpm run chore:update-build-info
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -209,9 +209,9 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
|
||||
12
.github/workflows/nightly.yml
vendored
12
.github/workflows/nightly.yml
vendored
@@ -19,6 +19,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GITHUB_UPLOAD_URL: https://uploads.github.com/repos/TriliumNext/Notes/releases/179589950/assets{?name,label}
|
||||
GITHUB_RELEASE_ID: 179589950
|
||||
|
||||
permissions:
|
||||
@@ -50,12 +51,13 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
- name: Update nightly version
|
||||
run: npm run chore:ci-update-nightly-version
|
||||
- name: Run the build
|
||||
@@ -77,7 +79,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
@@ -89,7 +91,7 @@ jobs:
|
||||
name: Nightly Build
|
||||
|
||||
- name: Publish artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
||||
@@ -118,7 +120,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
|
||||
26
.github/workflows/playwright.yml
vendored
26
.github/workflows/playwright.yml
vendored
@@ -4,8 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "apps/website/**"
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
@@ -21,21 +19,25 @@ jobs:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
|
||||
# - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: pnpm exec playwright install --with-deps
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
|
||||
- run: pnpm --filter server-e2e e2e
|
||||
|
||||
- name: Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: e2e report
|
||||
path: apps/server-e2e/test-output
|
||||
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
|
||||
# - run: npx nx-cloud record -- echo Hello World
|
||||
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
|
||||
# When you enable task distribution, run the e2e-ci task instead of e2e
|
||||
- run: pnpm exec nx affected -t e2e --exclude desktop-e2e
|
||||
|
||||
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@@ -30,30 +30,18 @@ jobs:
|
||||
image: win-signing
|
||||
shell: cmd
|
||||
forge_platform: win32
|
||||
# Exclude ARM64 Linux from default matrix to use native runner
|
||||
exclude:
|
||||
- arch: arm64
|
||||
os:
|
||||
name: linux
|
||||
# Add ARM64 Linux with native ubuntu-24.04-arm runner for better-sqlite3 compatibility
|
||||
include:
|
||||
- arch: arm64
|
||||
os:
|
||||
name: linux
|
||||
image: ubuntu-24.04-arm
|
||||
shell: bash
|
||||
forge_platform: linux
|
||||
runs-on: ${{ matrix.os.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
- name: Run the build
|
||||
uses: ./.github/actions/build-electron
|
||||
with:
|
||||
@@ -73,7 +61,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Upload the artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
||||
path: apps/desktop/upload/*.*
|
||||
@@ -100,7 +88,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Upload the artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-server-linux-${{ matrix.arch }}
|
||||
path: upload/*.*
|
||||
@@ -120,14 +108,14 @@ jobs:
|
||||
docs/Release Notes
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: release-*
|
||||
path: upload
|
||||
|
||||
- name: Publish stable release
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
with:
|
||||
draft: false
|
||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||
|
||||
67
.github/workflows/sync-docs-to-wiki.yml
vendored
Normal file
67
.github/workflows/sync-docs-to-wiki.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Sync Docs to Wiki
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
permissions:
|
||||
contents: read # Read access to repository contents
|
||||
# Note: Writing to wiki requires a PAT or GITHUB_TOKEN with additional permissions
|
||||
# The default GITHUB_TOKEN cannot write to wikis, so we need to:
|
||||
# 1. Create a Personal Access Token (PAT) with 'repo' scope
|
||||
# 2. Add it as a repository secret named WIKI_TOKEN
|
||||
|
||||
jobs:
|
||||
sync-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main-repo
|
||||
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: TriliumNext/Trilium.wiki
|
||||
path: wiki
|
||||
token: ${{ secrets.WIKI_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install tsx for TypeScript execution
|
||||
run: npm install -g tsx
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
|
||||
- name: Sync documentation to wiki
|
||||
id: sync
|
||||
run: |
|
||||
tsx main-repo/.github/scripts/sync-docs-to-wiki.ts
|
||||
env:
|
||||
MAIN_REPO_PATH: main-repo
|
||||
WIKI_PATH: wiki
|
||||
|
||||
- name: Commit and push changes
|
||||
if: contains(steps.sync.outputs.changes, 'true')
|
||||
run: |
|
||||
cd wiki
|
||||
git add .
|
||||
git commit -m "Sync documentation from main repository
|
||||
|
||||
Source commit: ${{ github.sha }}
|
||||
Triggered by: ${{ github.event.head_commit.message }}"
|
||||
git push
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WIKI_TOKEN }}
|
||||
51
.github/workflows/website.yml
vendored
51
.github/workflows/website.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: Deploy website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/website/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/website/**"
|
||||
|
||||
release:
|
||||
types: [ released ]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build & deploy website
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write # For PR preview comments
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter website --frozen-lockfile
|
||||
|
||||
- name: Build the website
|
||||
run: pnpm website:build
|
||||
|
||||
- name: Deploy
|
||||
uses: ./.github/actions/deploy-to-cloudflare-pages
|
||||
with:
|
||||
project_name: "trilium-homepage"
|
||||
comment_body: "📚 Website preview is ready"
|
||||
production_url: "https://triliumnotes.org"
|
||||
deploy_dir: "apps/website/dist"
|
||||
cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
/.cache
|
||||
|
||||
# compiled output
|
||||
dist
|
||||
@@ -33,11 +32,14 @@ testem.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
test-output
|
||||
|
||||
apps/*/data*
|
||||
apps/*/data
|
||||
apps/*/out
|
||||
upload
|
||||
|
||||
@@ -45,7 +47,4 @@ upload
|
||||
*.tsbuildinfo
|
||||
|
||||
/result
|
||||
.svelte-kit
|
||||
|
||||
# docs
|
||||
site/
|
||||
.svelte-kit
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -5,6 +5,7 @@
|
||||
"lokalise.i18n-ally",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-playwright.playwright",
|
||||
"nrwl.angular-console",
|
||||
"redhat.vscode-yaml",
|
||||
"tobermory.es6-string-html",
|
||||
"vitest.explorer",
|
||||
|
||||
1
.vscode/i18n-ally-custom-framework.yml
vendored
1
.vscode/i18n-ally-custom-framework.yml
vendored
@@ -14,7 +14,6 @@ usageMatchRegex:
|
||||
# the `{key}` will be placed by a proper keypath matching regex,
|
||||
# you can ignore it and use your own matching rules as well
|
||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||
- <Trans\s*i18nKey="({key})"[^>]*>
|
||||
|
||||
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
|
||||
# and works like how the i18next framework identifies the namespace scope from the
|
||||
|
||||
8
.vscode/mcp.json
vendored
Normal file
8
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"servers": {
|
||||
"nx-mcp": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:9461/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -5,8 +5,7 @@
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": [
|
||||
"apps/server/src/assets/translations",
|
||||
"apps/client/src/translations",
|
||||
"apps/website/public/translations"
|
||||
"apps/client/src/translations"
|
||||
],
|
||||
"npm.exclude": [
|
||||
"**/dist",
|
||||
@@ -36,5 +35,6 @@
|
||||
"docs/**/*.png": true,
|
||||
"apps/server/src/assets/doc_notes/**": true,
|
||||
"apps/edit-docs/demo/**": true
|
||||
}
|
||||
},
|
||||
"nxConsole.generateAiAgentRules": true
|
||||
}
|
||||
13
CLAUDE.md
13
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Overview
|
||||
|
||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
|
||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using NX, with multiple applications and shared packages.
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -14,9 +14,12 @@ Trilium Notes is a hierarchical note-taking application with advanced features l
|
||||
|
||||
### Running Applications
|
||||
- `pnpm run server:start` - Start development server (http://localhost:8080)
|
||||
- `pnpm nx run server:serve` - Alternative server start command
|
||||
- `pnpm nx run desktop:serve` - Run desktop Electron app
|
||||
- `pnpm run server:start-prod` - Run server in production mode
|
||||
|
||||
### Building
|
||||
- `pnpm nx build <project>` - Build specific project (server, client, desktop, etc.)
|
||||
- `pnpm run client:build` - Build client application
|
||||
- `pnpm run server:build` - Build server application
|
||||
- `pnpm run electron:build` - Build desktop application
|
||||
@@ -25,8 +28,13 @@ Trilium Notes is a hierarchical note-taking application with advanced features l
|
||||
- `pnpm test:all` - Run all tests (parallel + sequential)
|
||||
- `pnpm test:parallel` - Run tests that can run in parallel
|
||||
- `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math)
|
||||
- `pnpm nx test <project>` - Run tests for specific project
|
||||
- `pnpm coverage` - Generate coverage reports
|
||||
|
||||
### Linting & Type Checking
|
||||
- `pnpm nx run <project>:lint` - Lint specific project
|
||||
- `pnpm nx run <project>:typecheck` - Type check specific project
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Monorepo Structure
|
||||
@@ -86,6 +94,7 @@ Frontend uses a widget system (`apps/client/src/widgets/`):
|
||||
- `apps/server/src/assets/db/schema.sql` - Core database structure
|
||||
|
||||
4. **Configuration**:
|
||||
- `nx.json` - NX workspace configuration
|
||||
- `package.json` - Project dependencies and scripts
|
||||
|
||||
## Note Types and Features
|
||||
@@ -145,7 +154,7 @@ Trilium provides powerful user scripting capabilities:
|
||||
- Update schema in `apps/server/src/assets/db/schema.sql`
|
||||
|
||||
## Build System Notes
|
||||
- Uses pnpm for monorepo management
|
||||
- Uses NX for monorepo management with build caching
|
||||
- Vite for fast development builds
|
||||
- ESBuild for production optimization
|
||||
- pnpm workspaces for dependency management
|
||||
|
||||
83
README.md
83
README.md
@@ -1,14 +1,3 @@
|
||||
<div align="center">
|
||||
<sup>Special thanks to:</sup><br />
|
||||
<a href="https://go.warp.dev/Trilium" target="_blank">
|
||||
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png"><br />
|
||||
Warp, built for coding with multiple AI agents<br />
|
||||
</a>
|
||||
<sup>Available for macOS, Linux and Windows</sup>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
# Trilium Notes
|
||||
|
||||
 
|
||||
@@ -16,7 +5,7 @@
|
||||

|
||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
||||
|
||||
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md) | [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) | [Spanish](./docs/README-es.md)
|
||||
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
|
||||
|
||||
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
|
||||
|
||||
@@ -24,27 +13,6 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
|
||||
|
||||
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
|
||||
|
||||
## ⏬ Download
|
||||
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) – stable version, recommended for most users.
|
||||
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) – unstable development version, updated daily with the latest features and fixes.
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
**Visit our comprehensive documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
||||
|
||||
Our documentation is available in multiple formats:
|
||||
- **Online Documentation**: Browse the full documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)
|
||||
- **In-App Help**: Press `F1` within Trilium to access the same documentation directly in the application
|
||||
- **GitHub**: Navigate through the [User Guide](./docs/User%20Guide/User%20Guide/) in this repository
|
||||
|
||||
### Quick Links
|
||||
- [Getting Started Guide](https://docs.triliumnotes.org/)
|
||||
- [Installation Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
|
||||
- [Docker Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
|
||||
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
|
||||
- [Basic Concepts and Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
|
||||
- [Patterns of Personal Knowledge Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
|
||||
|
||||
## 🎁 Features
|
||||
|
||||
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
|
||||
@@ -88,6 +56,19 @@ There are no special migration steps to migrate from a zadam/Trilium instance to
|
||||
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
We're currently in the progress of moving the documentation to in-app (hit the `F1` key within Trilium). As a result, there may be some missing parts until we've completed the migration. If you'd prefer to navigate through the documentation within GitHub, you can navigate the [User Guide](./docs/User%20Guide/User%20Guide/) documentation.
|
||||
|
||||
Below are some quick links for your convenience to navigate the documentation:
|
||||
- [Server installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
|
||||
- [Docker installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
|
||||
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
|
||||
- [Concepts and Features - Note](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
|
||||
- [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
|
||||
|
||||
Until we finish reorganizing the documentation, you may also want to [browse the old documentation](https://triliumnext.github.io/Docs).
|
||||
|
||||
## 💬 Discuss with us
|
||||
|
||||
Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have!
|
||||
@@ -161,7 +142,7 @@ Download the repository, install dependencies using `pnpm` and then run the envi
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm edit-docs:edit-docs
|
||||
pnpm nx run edit-docs:edit-docs
|
||||
```
|
||||
|
||||
### Building the Executable
|
||||
@@ -170,7 +151,7 @@ Download the repository, install dependencies using `pnpm` and then build the de
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
|
||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
||||
```
|
||||
|
||||
For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
|
||||
@@ -181,34 +162,16 @@ Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blo
|
||||
|
||||
## 👏 Shoutouts
|
||||
|
||||
* [zadam](https://github.com/zadam) for the original concept and implementation of the application.
|
||||
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the application icon.
|
||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight widget.
|
||||
* [Dosu](https://dosu.dev/) for providing us with the automated responses to GitHub issues and discussions.
|
||||
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
|
||||
|
||||
Trilium would not be possible without the technologies behind it:
|
||||
|
||||
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind text notes. We are grateful for being offered a set of the premium features.
|
||||
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages.
|
||||
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite whiteboard used in Canvas notes.
|
||||
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the mind map functionality.
|
||||
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps.
|
||||
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive table used in collections.
|
||||
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library without real competition.
|
||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
|
||||
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
|
||||
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
|
||||
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
|
||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
|
||||
|
||||
## 🤝 Support
|
||||
|
||||
Trilium is built and maintained with [hundreds of hours of work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your support keeps it open-source, improves features, and covers costs such as hosting.
|
||||
|
||||
Consider supporting the main developer ([eliandoran](https://github.com/eliandoran)) of the application via:
|
||||
|
||||
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
|
||||
- [PayPal](https://paypal.me/eliandoran)
|
||||
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
|
||||
Support for the TriliumNext organization will be possible in the near future. For now, you can:
|
||||
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/trilium/graphs/contributors))) for a full list)
|
||||
- Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
|
||||
|
||||
|
||||
## 🔑 License
|
||||
|
||||
@@ -35,20 +35,22 @@
|
||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.56.1",
|
||||
"@stylistic/eslint-plugin": "5.5.0",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/node": "24.9.2",
|
||||
"@types/yargs": "17.0.34",
|
||||
"@playwright/test": "1.55.0",
|
||||
"@stylistic/eslint-plugin": "5.3.1",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.18.0",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.39.0",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.5",
|
||||
"jsdoc": "4.0.4",
|
||||
"lorem-ipsum": "2.0.8",
|
||||
"rcedit": "4.0.1",
|
||||
"rimraf": "6.1.0",
|
||||
"tslib": "2.8.1"
|
||||
"rimraf": "6.0.1",
|
||||
"tslib": "2.8.1",
|
||||
"typedoc": "0.28.12",
|
||||
"typedoc-plugin-missing-exports": "4.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"appdmg": "0.6.6"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type child_process from "child_process";
|
||||
import { describe, beforeAll, afterAll } from "vitest";
|
||||
|
||||
let etapiAuthToken: string | undefined;
|
||||
@@ -11,6 +12,8 @@ type SpecDefinitionsFunc = () => void;
|
||||
|
||||
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
|
||||
describe(description, () => {
|
||||
let appProcess: ReturnType<typeof child_process.spawn>;
|
||||
|
||||
beforeAll(async () => {});
|
||||
|
||||
afterAll(() => {});
|
||||
|
||||
15
_regroup/typedoc.json
Normal file
15
_regroup/typedoc.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"entryPoints": [
|
||||
"src/services/backend_script_entrypoint.ts",
|
||||
"src/public/app/services/frontend_script_entrypoint.ts"
|
||||
],
|
||||
"plugin": [
|
||||
"typedoc-plugin-missing-exports"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "html",
|
||||
"path": "./docs/Script API"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "build-docs",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/main.ts",
|
||||
"scripts": {
|
||||
"start": "tsx ."
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.19.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.10.0",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"typedoc": "0.28.14",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* The backend script API is accessible to code notes with the "JS (backend)" language.
|
||||
*
|
||||
* The entire API is exposed as a single global: {@link api}
|
||||
*
|
||||
* @module Backend Script API
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
||||
* script note on the server side.
|
||||
*
|
||||
* 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 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";
|
||||
export type { BNote };
|
||||
export type { default as BOption } from "../../server/src/becca/entities/boption.js";
|
||||
export type { default as BRecentNote } from "../../server/src/becca/entities/brecent_note.js";
|
||||
export type { default as BRevision } from "../../server/src/becca/entities/brevision.js";
|
||||
|
||||
import BNote from "../../server/src/becca/entities/bnote.js";
|
||||
import BackendScriptApi, { type Api } from "../../server/src/services/backend_script_api.js";
|
||||
|
||||
export type { Api };
|
||||
|
||||
const fakeNote = new BNote();
|
||||
|
||||
/**
|
||||
* 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,127 +0,0 @@
|
||||
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
|
||||
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 * 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 BuildContext from "./context.js";
|
||||
|
||||
const DOCS_ROOT = "../../../docs";
|
||||
const OUTPUT_DIR = "../../site";
|
||||
|
||||
async function buildDocsInner() {
|
||||
const i18n = await import("@triliumnext/server/src/services/i18n.js");
|
||||
await i18n.initializeTranslations();
|
||||
|
||||
const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
|
||||
await sqlInit.createInitialDatabase(true);
|
||||
|
||||
const note = await importData(join(__dirname, DOCS_ROOT, "User Guide"));
|
||||
|
||||
// Export
|
||||
const zipFilePath = "output.zip";
|
||||
try {
|
||||
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 fileOutputStream = fsExtra.createWriteStream(zipFilePath);
|
||||
await exportToZip(taskContext, branch, "share", fileOutputStream);
|
||||
await waitForStreamToFinish(fileOutputStream);
|
||||
await extractZip(zipFilePath, OUTPUT_DIR);
|
||||
} finally {
|
||||
if (await fsExtra.exists(zipFilePath)) {
|
||||
await fsExtra.rm(zipFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy favicon.
|
||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
|
||||
|
||||
console.log("Documentation built successfully!");
|
||||
}
|
||||
|
||||
export async function importData(path: string) {
|
||||
const buffer = await createImportZip(path);
|
||||
const importService = (await import("@triliumnext/server/src/services/import/zip.js")).default;
|
||||
const TaskContext = (await import("@triliumnext/server/src/services/task_context.js")).default;
|
||||
const context = new TaskContext("no-progress-reporting", "importNotes", null);
|
||||
const becca = (await import("@triliumnext/server/src/becca/becca.js")).default;
|
||||
|
||||
const rootNote = becca.getRoot();
|
||||
if (!rootNote) {
|
||||
throw new Error("Missing root note for import.");
|
||||
}
|
||||
return await importService.importZip(context, buffer, rootNote, {
|
||||
preserveIds: true
|
||||
});
|
||||
}
|
||||
|
||||
async function createImportZip(path: string) {
|
||||
const inputFile = "input.zip";
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 0 }
|
||||
});
|
||||
|
||||
console.log("Archive path is ", resolve(path))
|
||||
archive.directory(path, "/");
|
||||
|
||||
const outputStream = fsExtra.createWriteStream(inputFile);
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
await waitForStreamToFinish(outputStream);
|
||||
|
||||
try {
|
||||
return await fsExtra.readFile(inputFile);
|
||||
} finally {
|
||||
await fsExtra.rm(inputFile);
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
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)) {
|
||||
const destPath = join(outputPath, entry.fileName);
|
||||
const fileContent = await readContent(zip, entry);
|
||||
|
||||
await fsExtra.mkdirs(dirname(destPath));
|
||||
await fs.writeFile(destPath, fileContent);
|
||||
}
|
||||
|
||||
zip.readEntry();
|
||||
});
|
||||
}
|
||||
|
||||
export default async function buildDocs({ gitRootDir }: BuildContext) {
|
||||
// Build the share theme.
|
||||
execSync(`pnpm run --filter share-theme build`, {
|
||||
stdio: "inherit",
|
||||
cwd: gitRootDir
|
||||
});
|
||||
|
||||
// Trigger the actual build.
|
||||
await new Promise((res, rej) => {
|
||||
cls.init(() => {
|
||||
buildDocsInner()
|
||||
.catch(rej)
|
||||
.then(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export default interface BuildContext {
|
||||
gitRootDir: string;
|
||||
baseDir: string;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* The front script API is accessible to code notes with the "JS (frontend)" language.
|
||||
*
|
||||
* The entire API is exposed as a single global: {@link api}
|
||||
*
|
||||
* @module Frontend Script API
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
||||
* script note.
|
||||
*
|
||||
* 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 RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
|
||||
|
||||
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
|
||||
|
||||
//@ts-expect-error
|
||||
export const api: Api = new FrontendScriptApi();
|
||||
@@ -1,26 +0,0 @@
|
||||
import { join } from "path";
|
||||
import BuildContext from "./context";
|
||||
import buildSwagger from "./swagger";
|
||||
import { existsSync, mkdirSync, rmSync } from "fs";
|
||||
import buildDocs from "./build-docs";
|
||||
import buildScriptApi from "./script-api";
|
||||
|
||||
const context: BuildContext = {
|
||||
gitRootDir: join(__dirname, "../../../"),
|
||||
baseDir: join(__dirname, "../../../site")
|
||||
};
|
||||
|
||||
async function main() {
|
||||
// Clean input dir.
|
||||
if (existsSync(context.baseDir)) {
|
||||
rmSync(context.baseDir, { recursive: true });
|
||||
}
|
||||
mkdirSync(context.baseDir);
|
||||
|
||||
// Start building.
|
||||
await buildDocs(context);
|
||||
buildSwagger(context);
|
||||
buildScriptApi(context);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,15 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
import BuildContext from "./context";
|
||||
import { join } from "path";
|
||||
|
||||
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
|
||||
// Generate types
|
||||
execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });
|
||||
|
||||
for (const config of [ "backend", "frontend" ]) {
|
||||
const outDir = join(baseDir, "script-api", config);
|
||||
execSync(`pnpm typedoc --options typedoc.${config}.json --html "${outDir}"`, {
|
||||
stdio: "inherit"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import BuildContext from "./context";
|
||||
import { join } from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { mkdirSync } from "fs";
|
||||
|
||||
interface BuildInfo {
|
||||
specPath: string;
|
||||
outDir: string;
|
||||
}
|
||||
|
||||
const DIR_PREFIX = "rest-api";
|
||||
|
||||
const buildInfos: BuildInfo[] = [
|
||||
{
|
||||
// Paths are relative to Git root.
|
||||
specPath: "apps/server/internal.openapi.yaml",
|
||||
outDir: `${DIR_PREFIX}/internal`
|
||||
},
|
||||
{
|
||||
specPath: "apps/server/etapi.openapi.yaml",
|
||||
outDir: `${DIR_PREFIX}/etapi`
|
||||
}
|
||||
];
|
||||
|
||||
export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
|
||||
for (const { specPath, outDir } of buildInfos) {
|
||||
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" });
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"target": "ES2020",
|
||||
"outDir": "dist",
|
||||
"strict": false,
|
||||
"types": [
|
||||
"node",
|
||||
"express"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../server/src/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"eslint.config.js",
|
||||
"eslint.config.cjs",
|
||||
"eslint.config.mjs"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../server/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "../desktop/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "../client/tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../server"
|
||||
},
|
||||
{
|
||||
"path": "../client"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"name": "Trilium Backend API",
|
||||
"entryPoints": [
|
||||
"src/backend_script_entrypoint.ts"
|
||||
],
|
||||
"plugin": [
|
||||
"typedoc-plugin-missing-exports"
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"name": "Trilium Frontend API",
|
||||
"entryPoints": [
|
||||
"src/frontend_script_entrypoint.ts"
|
||||
],
|
||||
"plugin": [
|
||||
"typedoc-plugin-missing-exports"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
# The development license key for premium CKEditor features.
|
||||
# Note: This key must only be used for the Trilium Notes project.
|
||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3ODcyNzA0MDAsImp0aSI6IjkyMWE1MWNlLTliNDMtNGRlMC1iOTQwLTc5ZjM2MDBkYjg1NyIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOiJ0cmlsaXVtIiwiZmVhdHVyZXMiOlsiVFJJTElVTSJdLCJ2YyI6ImU4YzRhMjBkIn0.hny77p-U4-jTkoqbwPytrEar5ylGCWBN7Ez3SlB8i6_mJCBIeCSTOlVQk_JMiOEq3AGykUMHzWXzjdMFwgniOw
|
||||
# Expires on: 2025-09-13
|
||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
|
||||
VITE_CKEDITOR_ENABLE_INSPECTOR=false
|
||||
@@ -1,21 +1,16 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.99.3",
|
||||
"version": "0.98.1",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": {
|
||||
"name": "Trilium Notes Team",
|
||||
"email": "contact@eliandoran.me",
|
||||
"url": "https://github.com/TriliumNext/Trilium"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
||||
"test": "vitest",
|
||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.39.0",
|
||||
"@eslint/js": "9.34.0",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
@@ -24,7 +19,7 @@
|
||||
"@fullcalendar/multimonth": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mermaid-js/layout-elk": "0.1.9",
|
||||
"@mind-elixir/node-menu": "5.0.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
@@ -32,35 +27,33 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
"color": "5.0.2",
|
||||
"dayjs": "1.11.19",
|
||||
"dayjs": "1.11.18",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "2.2.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "16.5.0",
|
||||
"i18next": "25.6.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.4.2",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.25",
|
||||
"katex": "0.16.22",
|
||||
"knockout": "3.5.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "16.4.1",
|
||||
"mermaid": "11.12.1",
|
||||
"mind-elixir": "5.3.4",
|
||||
"marked": "16.2.1",
|
||||
"mermaid": "11.10.1",
|
||||
"mind-elixir": "5.0.6",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.2",
|
||||
"react-i18next": "16.2.3",
|
||||
"reveal.js": "5.2.1",
|
||||
"preact": "10.27.1",
|
||||
"react-i18next": "15.7.3",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4"
|
||||
@@ -70,14 +63,26 @@
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/leaflet": "1.9.21",
|
||||
"@types/leaflet-gpx": "1.3.8",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/reveal.js": "5.2.1",
|
||||
"@types/tabulator-tables": "6.3.0",
|
||||
"@types/tabulator-tables": "6.2.10",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.0.10",
|
||||
"happy-dom": "18.0.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
"vite-plugin-static-copy": "3.1.2"
|
||||
},
|
||||
"nx": {
|
||||
"name": "client",
|
||||
"targets": {
|
||||
"serve": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"circular-deps": {
|
||||
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,6 @@
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"display_override": [
|
||||
"window-controls-overlay"
|
||||
],
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon.png",
|
||||
|
||||
@@ -116,7 +116,7 @@ export type CommandMappings = {
|
||||
openedFileUpdated: CommandData & {
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
lastModifiedMs?: number;
|
||||
lastModifiedMs: number;
|
||||
filePath: string;
|
||||
};
|
||||
focusAndSelectTitle: CommandData & {
|
||||
@@ -218,12 +218,12 @@ export type CommandMappings = {
|
||||
/** Works only in the electron context menu. */
|
||||
replaceMisspelling: CommandData;
|
||||
|
||||
importMarkdownInline: CommandData;
|
||||
showPasswordNotSet: CommandData;
|
||||
showProtectedSessionPasswordDialog: CommandData;
|
||||
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
||||
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
|
||||
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
|
||||
showPasteMarkdownDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
|
||||
closeProtectedSessionPasswordDialog: CommandData;
|
||||
copyImageReferenceToClipboard: CommandData;
|
||||
copyImageToClipboard: CommandData;
|
||||
@@ -270,7 +270,6 @@ export type CommandMappings = {
|
||||
closeThisNoteSplit: CommandData;
|
||||
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
|
||||
jumpToNote: CommandData;
|
||||
openTodayNote: CommandData;
|
||||
commandPalette: CommandData;
|
||||
|
||||
// Keyboard shortcuts
|
||||
@@ -651,7 +650,7 @@ export class AppContext extends Component {
|
||||
}
|
||||
|
||||
getComponentByEl(el: HTMLElement) {
|
||||
return $(el).closest("[data-component-id]").prop("component");
|
||||
return $(el).closest(".component").prop("component");
|
||||
}
|
||||
|
||||
addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) {
|
||||
|
||||
@@ -159,16 +159,6 @@ export default class Entrypoints extends Component {
|
||||
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
|
||||
}
|
||||
|
||||
async openTodayNoteCommand() {
|
||||
const todayNote = await dateNoteService.getTodayNote();
|
||||
if (!todayNote) {
|
||||
console.warn("Missing today note.");
|
||||
return;
|
||||
}
|
||||
|
||||
await appContext.tabManager.openInSameTab(todayNote.noteId);
|
||||
}
|
||||
|
||||
async runActiveNoteCommand() {
|
||||
const noteContext = appContext.tabManager.getActiveContext();
|
||||
if (!noteContext) {
|
||||
|
||||
@@ -326,11 +326,9 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
}
|
||||
|
||||
// Collections must always display a note list, even if no children.
|
||||
if (note.type === "book") {
|
||||
const viewType = note.getLabelValue("viewType") ?? "grid";
|
||||
if (!["list", "grid"].includes(viewType)) {
|
||||
return true;
|
||||
}
|
||||
const viewType = note.getLabelValue("viewType") ?? "grid";
|
||||
if (!["list", "grid"].includes(viewType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!note.hasChildren()) {
|
||||
@@ -440,22 +438,4 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
}
|
||||
}
|
||||
|
||||
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
|
||||
const ntxId = $(evt?.target as Element)
|
||||
.closest("[data-ntx-id]")
|
||||
.attr("data-ntx-id");
|
||||
|
||||
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
|
||||
|
||||
if (noteContext) {
|
||||
noteContext.setNote(notePath, { viewScope }).then(() => {
|
||||
if (noteContext !== appContext.tabManager.getActiveContext()) {
|
||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteContext;
|
||||
|
||||
@@ -433,9 +433,6 @@ export default class TabManager extends Component {
|
||||
$autocompleteEl.autocomplete("close");
|
||||
}
|
||||
|
||||
// close dangling tooltips
|
||||
$("body > div.tooltip").remove();
|
||||
|
||||
const noteContextsToRemove = noteContextToRemove.getSubContexts();
|
||||
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId);
|
||||
|
||||
@@ -603,18 +600,18 @@ export default class TabManager extends Component {
|
||||
}
|
||||
|
||||
async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) {
|
||||
const { notePath, hoistedNoteId, viewScope } = this.getNoteContextById(ntxId);
|
||||
const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
|
||||
|
||||
const removed = await this.removeNoteContext(ntxId);
|
||||
|
||||
if (removed) {
|
||||
this.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||
this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
|
||||
}
|
||||
}
|
||||
|
||||
async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) {
|
||||
const { notePath, hoistedNoteId, viewScope } = this.getNoteContextById(ntxId);
|
||||
this.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||
const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
|
||||
this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
|
||||
}
|
||||
|
||||
async reopenLastTabCommand() {
|
||||
|
||||
@@ -23,11 +23,11 @@ export default class TouchBarComponent extends Component {
|
||||
this.$widget = $("<div>");
|
||||
|
||||
$(window).on("focusin", async (e) => {
|
||||
const focusedEl = e.target as unknown as HTMLElement;
|
||||
const $target = $(focusedEl);
|
||||
const $target = $(e.target);
|
||||
|
||||
this.$activeModal = $target.closest(".modal-dialog");
|
||||
this.lastFocusedComponent = appContext.getComponentByEl(focusedEl);
|
||||
const parentComponentEl = $target.closest(".component");
|
||||
this.lastFocusedComponent = appContext.getComponentByEl(parentComponentEl[0]);
|
||||
this.#refreshTouchBar();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { t } from "./services/i18n.js";
|
||||
import options from "./services/options.js";
|
||||
import type ElectronRemote from "@electron/remote";
|
||||
import type Electron from "electron";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
import "boxicons/css/boxicons.min.css";
|
||||
import "autocomplete.js/index_jquery.js";
|
||||
|
||||
@@ -44,10 +45,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));
|
||||
@@ -116,20 +113,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,5 +1,6 @@
|
||||
import server from "../services/server.js";
|
||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||
import ws from "../services/ws.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import cssClassManager from "../services/css_class_manager.js";
|
||||
import type { Froca } from "../services/froca-interface.js";
|
||||
@@ -255,20 +256,18 @@ export default class FNote {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
async getSubtreeNoteIds(includeArchived = false) {
|
||||
async getSubtreeNoteIds() {
|
||||
let noteIds: (string | string[])[] = [];
|
||||
for (const child of await this.getChildNotes()) {
|
||||
if (child.isArchived && !includeArchived) continue;
|
||||
|
||||
noteIds.push(child.noteId);
|
||||
noteIds.push(await child.getSubtreeNoteIds(includeArchived));
|
||||
noteIds.push(await child.getSubtreeNoteIds());
|
||||
}
|
||||
return noteIds.flat();
|
||||
}
|
||||
|
||||
async getSubtreeNotes() {
|
||||
const noteIds = await this.getSubtreeNoteIds();
|
||||
return (await this.froca.getNotes(noteIds));
|
||||
return this.froca.getNotes(noteIds);
|
||||
}
|
||||
|
||||
async getChildNotes() {
|
||||
@@ -417,7 +416,7 @@ export default class FNote {
|
||||
return notePaths;
|
||||
}
|
||||
|
||||
getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] {
|
||||
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
|
||||
const isHoistedRoot = hoistedNoteId === "root";
|
||||
|
||||
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
||||
@@ -428,23 +427,7 @@ export default class FNote {
|
||||
isHidden: path.includes("_hidden")
|
||||
}));
|
||||
|
||||
// Calculate the length of the prefix match between two arrays
|
||||
const prefixMatchLength = (path: string[], target: string[]) => {
|
||||
const diffIndex = path.findIndex((seg, i) => seg !== target[i]);
|
||||
return diffIndex === -1 ? Math.min(path.length, target.length) : diffIndex;
|
||||
};
|
||||
|
||||
notePaths.sort((a, b) => {
|
||||
if (activeNotePath) {
|
||||
const activeSegments = activeNotePath.split('/');
|
||||
const aOverlap = prefixMatchLength(a.notePath, activeSegments);
|
||||
const bOverlap = prefixMatchLength(b.notePath, activeSegments);
|
||||
// Paths with more matching prefix segments are prioritized
|
||||
// when the match count is equal, other criteria are used for sorting
|
||||
if (bOverlap !== aOverlap) {
|
||||
return bOverlap - aOverlap;
|
||||
}
|
||||
}
|
||||
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
|
||||
return a.isInHoistedSubTree ? -1 : 1;
|
||||
} else if (a.isArchived !== b.isArchived) {
|
||||
@@ -465,11 +448,10 @@ export default class FNote {
|
||||
* Returns the note path considered to be the "best"
|
||||
*
|
||||
* @param {string} [hoistedNoteId='root']
|
||||
* @param {string|null} [activeNotePath=null]
|
||||
* @return {string[]} array of noteIds constituting the particular note path
|
||||
*/
|
||||
getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) {
|
||||
return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath;
|
||||
getBestNotePath(hoistedNoteId = "root") {
|
||||
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -602,7 +584,7 @@ export default class FNote {
|
||||
let childBranches = this.getChildBranches();
|
||||
|
||||
if (!childBranches) {
|
||||
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);
|
||||
ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -923,8 +905,8 @@ export default class FNote {
|
||||
return this.getBlob();
|
||||
}
|
||||
|
||||
getBlob() {
|
||||
return this.froca.getBlob("notes", this.noteId);
|
||||
async getBlob() {
|
||||
return await this.froca.getBlob("notes", this.noteId);
|
||||
}
|
||||
|
||||
toString() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
@@ -41,7 +42,6 @@ import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||
import ApiLog from "../widgets/api_log.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@@ -123,10 +123,10 @@ export default class DesktopLayout {
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(new SpacerWidget(0, 1))
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
.child(<ClosePaneButton />)
|
||||
.child(<CreatePaneButton />)
|
||||
.child(new MovePaneButton(true))
|
||||
.child(new MovePaneButton(false))
|
||||
.child(new ClosePaneButton())
|
||||
.child(new CreatePaneButton())
|
||||
)
|
||||
.child(<Ribbon />)
|
||||
.child(<SharedInfo />)
|
||||
@@ -138,7 +138,7 @@ export default class DesktopLayout {
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(new NoteDetailWidget())
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(new NoteListWidget(false))
|
||||
.child(<SearchResult />)
|
||||
.child(<SqlResults />)
|
||||
.child(<ScrollPadding />)
|
||||
|
||||
@@ -27,11 +27,10 @@ import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import NoteIconWidget from "../widgets/note_icon";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
rootContainer
|
||||
@@ -64,9 +63,9 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />))
|
||||
.child(<StandaloneRibbonAdapter component={FormattingToolbar} />)
|
||||
.child(<PopupEditorFormattingToolbar />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(<NoteList media="screen" displayOnlyCollections />))
|
||||
.child(new NoteListWidget(true)))
|
||||
.child(<CallToActionDialog />);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
@@ -21,12 +22,7 @@ import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -43,8 +39,8 @@ kbd {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
padding-inline-start: 0.5em;
|
||||
padding-inline-end: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
.quick-search {
|
||||
@@ -62,7 +58,7 @@ const FANCYTREE_CSS = `
|
||||
margin-top: 0px;
|
||||
overflow-y: auto;
|
||||
contain: content;
|
||||
padding-inline-start: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
@@ -71,7 +67,7 @@ const FANCYTREE_CSS = `
|
||||
|
||||
.fancytree-title {
|
||||
font-size: 1.5em;
|
||||
margin-inline-start: 0.6em !important;
|
||||
margin-left: 0.6em !important;
|
||||
}
|
||||
|
||||
.fancytree-node {
|
||||
@@ -84,7 +80,7 @@ const FANCYTREE_CSS = `
|
||||
|
||||
span.fancytree-expander {
|
||||
width: 24px !important;
|
||||
margin-inline-end: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander {
|
||||
@@ -104,7 +100,7 @@ span.fancytree-expander {
|
||||
.tree-wrapper .scroll-to-active-note-button,
|
||||
.tree-wrapper .tree-settings-button {
|
||||
position: fixed;
|
||||
margin-inline-end: 16px;
|
||||
margin-right: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -129,41 +125,36 @@ export default class MobileLayout {
|
||||
.class("d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3")
|
||||
.id("mobile-sidebar-wrapper")
|
||||
.css("max-height", "100%")
|
||||
.css("padding-inline-start", "0")
|
||||
.css("padding-inline-end", "0")
|
||||
.css("padding-left", "0")
|
||||
.css("padding-right", "0")
|
||||
.css("contain", "content")
|
||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
|
||||
)
|
||||
.child(
|
||||
new ScreenContainer("detail", "row")
|
||||
new ScreenContainer("detail", "column")
|
||||
.id("detail-container")
|
||||
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
|
||||
.child(
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<SharedInfoWidget />)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new NoteDetailWidget())
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||
.child(<SearchResult />)
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
new FlexContainer("row")
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<SharedInfoWidget />)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
)
|
||||
)
|
||||
.child(
|
||||
@@ -192,4 +183,4 @@ function FilePropertiesWrapper() {
|
||||
{note?.type === "file" && <FilePropertiesTab note={note} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
|
||||
// @ts-ignore - module = undefined
|
||||
// Required for correct loading of scripts in Electron
|
||||
if (typeof module === 'object') {window.module = module; module = undefined;}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
||||
import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { should } from "vitest";
|
||||
|
||||
export interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
@@ -15,13 +13,8 @@ export interface ContextMenuOptions<T> {
|
||||
onHide?: () => void;
|
||||
}
|
||||
|
||||
export interface MenuSeparatorItem {
|
||||
kind: "separator";
|
||||
}
|
||||
|
||||
export interface MenuHeader {
|
||||
title: string;
|
||||
kind: "header";
|
||||
interface MenuSeparatorItem {
|
||||
title: "----";
|
||||
}
|
||||
|
||||
export interface MenuItemBadge {
|
||||
@@ -45,13 +38,12 @@ export interface MenuCommandItem<T> {
|
||||
handler?: MenuHandler<T>;
|
||||
items?: MenuItem<T>[] | null;
|
||||
shortcut?: string;
|
||||
keyboardShortcut?: KeyboardActionNames;
|
||||
spellingSuggestion?: string;
|
||||
checked?: boolean;
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader;
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||
|
||||
@@ -150,57 +142,20 @@ class ContextMenu {
|
||||
this.$widget
|
||||
.css({
|
||||
display: "block",
|
||||
top,
|
||||
left
|
||||
top: top,
|
||||
left: left
|
||||
})
|
||||
.addClass("show");
|
||||
}
|
||||
|
||||
addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[], multicolumn = false) {
|
||||
let $group = $parent; // The current group or parent element to which items are being appended
|
||||
let shouldStartNewGroup = false; // If true, the next item will start a new group
|
||||
let shouldResetGroup = false; // If true, the next item will be the last one from the group
|
||||
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index];
|
||||
addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[]) {
|
||||
for (const item of items) {
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the current item is a header, start a new group. This group will contain the
|
||||
// header and the next item that follows the header.
|
||||
if ("kind" in item && item.kind === "header") {
|
||||
if (multicolumn && !shouldResetGroup) {
|
||||
shouldStartNewGroup = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the next item is a separator, start a new group. This group will contain the
|
||||
// current item, the separator, and the next item after the separator.
|
||||
const nextItem = (index < items.length - 1) ? items[index + 1] : null;
|
||||
if (multicolumn && nextItem && "kind" in nextItem && nextItem.kind === "separator") {
|
||||
if (!shouldResetGroup) {
|
||||
shouldStartNewGroup = true;
|
||||
} else {
|
||||
shouldResetGroup = true; // Continue the current group
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new group to avoid column breaks before and after the seaparator / header.
|
||||
// This is a workaround for Firefox not supporting break-before / break-after: avoid
|
||||
// for columns.
|
||||
if (shouldStartNewGroup) {
|
||||
$group = $("<div class='dropdown-no-break'>");
|
||||
$parent.append($group);
|
||||
shouldStartNewGroup = false;
|
||||
}
|
||||
|
||||
if ("kind" in item && item.kind === "separator") {
|
||||
$group.append($("<div>").addClass("dropdown-divider"));
|
||||
shouldResetGroup = true; // End the group after the next item
|
||||
} else if ("kind" in item && item.kind === "header") {
|
||||
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
|
||||
shouldResetGroup = true;
|
||||
if (item.title === "----") {
|
||||
$parent.append($("<div>").addClass("dropdown-divider"));
|
||||
} else {
|
||||
const $icon = $("<span>");
|
||||
|
||||
@@ -230,23 +185,7 @@ class ContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
||||
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
|
||||
if (shortcuts) {
|
||||
const allShortcuts: string[] = [];
|
||||
for (const effectiveShortcut of shortcuts) {
|
||||
allShortcuts.push(effectiveShortcut.split("+")
|
||||
.map(key => `<kbd>${key}</kbd>`)
|
||||
.join("+"));
|
||||
}
|
||||
|
||||
if (allShortcuts.length) {
|
||||
const container = $("<span>").addClass("keyboard-shortcut");
|
||||
container.append($(allShortcuts.join(",")));
|
||||
$link.append(container);
|
||||
}
|
||||
}
|
||||
} else if ("shortcut" in item && item.shortcut) {
|
||||
if ("shortcut" in item && item.shortcut) {
|
||||
$link.append($("<kbd>").text(item.shortcut));
|
||||
}
|
||||
|
||||
@@ -302,24 +241,16 @@ class ContextMenu {
|
||||
$link.addClass("dropdown-toggle");
|
||||
|
||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||
const hasColumns = !!item.columns && item.columns > 1;
|
||||
if (!this.isMobile && hasColumns) {
|
||||
$subMenu.css("column-count", item.columns!);
|
||||
if (!this.isMobile && item.columns) {
|
||||
$subMenu.css("column-count", item.columns);
|
||||
}
|
||||
|
||||
this.addItems($subMenu, item.items, hasColumns);
|
||||
this.addItems($subMenu, item.items);
|
||||
|
||||
$item.append($subMenu);
|
||||
}
|
||||
|
||||
$group.append($item);
|
||||
|
||||
// After adding a menu item, if the previous item was a separator or header,
|
||||
// reset the group so that the next item will be appended directly to the parent.
|
||||
if (shouldResetGroup) {
|
||||
$group = $parent;
|
||||
shouldResetGroup = false;
|
||||
};
|
||||
$parent.append($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ function setupContextMenu() {
|
||||
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
||||
});
|
||||
|
||||
items.push({ kind: "separator" });
|
||||
items.push({ title: `----` });
|
||||
}
|
||||
|
||||
if (params.isEditable) {
|
||||
@@ -112,7 +112,7 @@ function setupContextMenu() {
|
||||
// Replace the placeholder with the real search keyword.
|
||||
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
|
||||
|
||||
items.push({ kind: "separator" });
|
||||
items.push({ title: "----" });
|
||||
|
||||
items.push({
|
||||
title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }),
|
||||
|
||||
@@ -45,16 +45,16 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
|
||||
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-script-launcher"), command: "addScriptLauncher", uiIcon: "bx bx-code-curly" } : null,
|
||||
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-custom-widget"), command: "addWidgetLauncher", uiIcon: "bx bx-customize" } : null,
|
||||
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-spacer"), command: "addSpacerLauncher", uiIcon: "bx bx-dots-horizontal" } : null,
|
||||
isVisibleRoot || isAvailableRoot ? { kind: "separator" } : null,
|
||||
isVisibleRoot || isAvailableRoot ? { title: "----" } : null,
|
||||
|
||||
isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
|
||||
isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
|
||||
isVisibleItem || isAvailableItem ? { kind: "separator" } : null,
|
||||
isVisibleItem || isAvailableItem ? { title: "----" } : null,
|
||||
|
||||
{ title: `${t("launcher_context_menu.duplicate-launcher")}`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem },
|
||||
{ title: `${t("launcher_context_menu.delete")}`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted },
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset }
|
||||
];
|
||||
|
||||
@@ -13,8 +13,6 @@ import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import type FAttachment from "../entities/fattachment.js";
|
||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||
import utils from "../services/utils.js";
|
||||
import attributes from "../services/attributes.js";
|
||||
import { executeBulkActions } from "../services/bulk_action.js";
|
||||
|
||||
// TODO: Deduplicate once client/server is well split.
|
||||
interface ConvertToAttachmentResponse {
|
||||
@@ -63,11 +61,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
// the only exception is when the only selected note is the one that was right-clicked, then
|
||||
// it's clear what the user meant to do.
|
||||
const selNodes = this.treeWidget.getSelectedNodes();
|
||||
const selectedNotes = await froca.getNotes(selNodes.map(node => node.data.noteId));
|
||||
if (note && !selectedNotes.includes(note)) selectedNotes.push(note);
|
||||
const isArchived = selectedNotes.every(note => note.isArchived);
|
||||
const canToggleArchived = !selectedNotes.some(note => note.isArchived !== isArchived);
|
||||
|
||||
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
|
||||
|
||||
const notSearch = note?.type !== "search";
|
||||
@@ -76,29 +69,27 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||
|
||||
const items: (MenuItem<TreeCommandNames> | null)[] = [
|
||||
{ title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
|
||||
{ title: `${t("tree-context-menu.open-in-a-new-tab")}`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
|
||||
|
||||
isHoisted
|
||||
? null
|
||||
: {
|
||||
title: `${t("tree-context-menu.hoist-note")}`,
|
||||
title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`,
|
||||
command: "toggleNoteHoisting",
|
||||
keyboardShortcut: "toggleNoteHoisting",
|
||||
uiIcon: "bx bxs-chevrons-up",
|
||||
enabled: noSelectedNotes && notSearch
|
||||
},
|
||||
!isHoisted || !isNotRoot
|
||||
? null
|
||||
: { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
: { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.insert-note-after"),
|
||||
title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`,
|
||||
command: "insertNoteAfter",
|
||||
keyboardShortcut: "createNoteAfter",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp,
|
||||
@@ -106,22 +97,21 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
},
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.insert-child-note"),
|
||||
title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`,
|
||||
command: "insertChildNote",
|
||||
keyboardShortcut: "createNoteInto",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
|
||||
columns: 2
|
||||
},
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{ title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
|
||||
|
||||
{ title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.advanced"),
|
||||
@@ -130,52 +120,48 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
items: [
|
||||
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true },
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.edit-branch-prefix"),
|
||||
title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`,
|
||||
command: "editBranchPrefix",
|
||||
keyboardShortcut: "editBranchPrefix",
|
||||
uiIcon: "bx bx-rename",
|
||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
||||
},
|
||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
{
|
||||
title: t("tree-context-menu.sort-by"),
|
||||
title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`,
|
||||
command: "sortChildNotes",
|
||||
keyboardShortcut: "sortChildNotes",
|
||||
uiIcon: "bx bx-sort-down",
|
||||
enabled: noSelectedNotes && notSearch
|
||||
},
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
|
||||
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
|
||||
]
|
||||
},
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.cut"),
|
||||
title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`,
|
||||
command: "cutNotesToClipboard",
|
||||
keyboardShortcut: "cutNotesToClipboard",
|
||||
uiIcon: "bx bx-cut",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch
|
||||
},
|
||||
|
||||
{ title: t("tree-context-menu.copy-clone"), command: "copyNotesToClipboard", keyboardShortcut: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted },
|
||||
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted },
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.paste-into"),
|
||||
title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`,
|
||||
command: "pasteNotesFromClipboard",
|
||||
keyboardShortcut: "pasteNotesFromClipboard",
|
||||
uiIcon: "bx bx-paste",
|
||||
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes
|
||||
},
|
||||
@@ -188,71 +174,39 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
},
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.move-to"),
|
||||
title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`,
|
||||
command: "moveNotesTo",
|
||||
keyboardShortcut: "moveNotesTo",
|
||||
uiIcon: "bx bx-transfer",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch
|
||||
},
|
||||
|
||||
{ title: t("tree-context-menu.clone-to"), command: "cloneNotesTo", keyboardShortcut: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
|
||||
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.duplicate"),
|
||||
title: `${t("tree-context-menu.duplicate")} <kbd data-command="duplicateSubtree">`,
|
||||
command: "duplicateSubtree",
|
||||
keyboardShortcut: "duplicateSubtree",
|
||||
uiIcon: "bx bx-outline",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
|
||||
},
|
||||
|
||||
{
|
||||
title: !isArchived ? t("tree-context-menu.archive") : t("tree-context-menu.unarchive"),
|
||||
uiIcon: !isArchived ? "bx bx-archive" : "bx bx-archive-out",
|
||||
enabled: canToggleArchived,
|
||||
handler: () => {
|
||||
if (!selectedNotes.length) return;
|
||||
|
||||
if (selectedNotes.length == 1) {
|
||||
const note = selectedNotes[0];
|
||||
if (!isArchived) {
|
||||
attributes.addLabel(note.noteId, "archived");
|
||||
} else {
|
||||
attributes.removeOwnedLabelByName(note, "archived");
|
||||
}
|
||||
} else {
|
||||
const noteIds = selectedNotes.map(note => note.noteId);
|
||||
if (!isArchived) {
|
||||
executeBulkActions(noteIds, [{
|
||||
name: "addLabel", labelName: "archived"
|
||||
}]);
|
||||
} else {
|
||||
executeBulkActions(noteIds, [{
|
||||
name: "deleteLabel", labelName: "archived"
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("tree-context-menu.delete"),
|
||||
title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
|
||||
command: "deleteNotes",
|
||||
keyboardShortcut: "deleteNotes",
|
||||
uiIcon: "bx bx-trash destructive-action-icon",
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
||||
},
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
||||
|
||||
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
||||
|
||||
{ kind: "separator" },
|
||||
{ title: "----" },
|
||||
|
||||
{
|
||||
title: t("tree-context-menu.search-in-subtree"),
|
||||
title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`,
|
||||
command: "searchInSubtree",
|
||||
keyboardShortcut: "searchInSubtree",
|
||||
uiIcon: "bx bx-search",
|
||||
enabled: notSearch && noSelectedNotes
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import appContext from "./components/app_context.js";
|
||||
import noteAutocompleteService from "./services/note_autocomplete.js";
|
||||
import glob from "./services/glob.js";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
import "boxicons/css/boxicons.min.css";
|
||||
import "autocomplete.js/index_jquery.js";
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
:root {
|
||||
--print-font-size: 11pt;
|
||||
--ck-content-color-image-caption-background: transparent !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 2cm;
|
||||
}
|
||||
|
||||
.note-list-widget.full-height,
|
||||
.note-list-widget.full-height .note-list-widget-content {
|
||||
height: unset !important;
|
||||
}
|
||||
|
||||
.component {
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
body[data-note-type="text"] .ck-content {
|
||||
font-size: var(--print-font-size);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ck-content figcaption {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ck-content a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ck-content a:not([href^="#root/"]) {
|
||||
text-decoration: underline;
|
||||
color: #374a75;
|
||||
}
|
||||
|
||||
.ck-content .todo-list__label * {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
|
||||
.ck-content .todo-list__label__description {
|
||||
/* The percentage of the line height that the check box occupies */
|
||||
--box-ratio: 0.75;
|
||||
/* The size of the gap between the check box and the caption */
|
||||
--box-text-gap: 0.25em;
|
||||
|
||||
--box-size: calc(1lh * var(--box-ratio));
|
||||
--box-vert-offset: calc((1lh - var(--box-size)) / 2);
|
||||
|
||||
display: inline-block;
|
||||
padding-inline-start: calc(var(--box-size) + var(--box-text-gap));
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-blank-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5C3.89%2c3 3%2c3.89 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5C21%2c3.89 20.1%2c3 19%2c3M19%2c5V19H5V5H19Z' /%3e%3c/svg%3e");
|
||||
background-position: 0 var(--box-vert-offset);
|
||||
background-size: var(--box-size);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.ck-content .todo-list__label:has(input[type="checkbox"]:checked) .todo-list__label__description {
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5A2%2c2 0 0%2c0 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5A2%2c2 0 0%2c0 19%2c3M19%2c5V19H5V5H19M10%2c17L6%2c13L7.41%2c11.58L10%2c14.17L16.59%2c7.58L18%2c9' /%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.ck-content .todo-list__label input[type="checkbox"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Footnotes */
|
||||
.footnote-reference a,
|
||||
.footnote-back-link a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
li.footnote-item {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.ck-content .footnote-back-link {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.ck-content .footnote-content {
|
||||
display: inline-block;
|
||||
width: unset;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Widows and orphans */
|
||||
p,
|
||||
blockquote {
|
||||
widows: 4;
|
||||
orphans: 4;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
widows: 6;
|
||||
orphans: 6;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tables */
|
||||
.table thead th,
|
||||
.table td,
|
||||
.table th {
|
||||
/* Fix center vertical alignment of table cells */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
pre {
|
||||
box-shadow: unset !important;
|
||||
border: 0.75pt solid gray !important;
|
||||
border-radius: 2pt !important;
|
||||
}
|
||||
|
||||
th,
|
||||
span[style] {
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Page breaks */
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
break-after: always;
|
||||
}
|
||||
|
||||
.page-break > *,
|
||||
.page-break::after {
|
||||
display: none !important;
|
||||
}
|
||||
/* #endregion */
|
||||
@@ -1,105 +0,0 @@
|
||||
import FNote from "./entities/fnote";
|
||||
import { render } from "preact";
|
||||
import { CustomNoteList } from "./widgets/collections/NoteList";
|
||||
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import content_renderer from "./services/content_renderer";
|
||||
|
||||
interface RendererProps {
|
||||
note: FNote;
|
||||
onReady: () => void;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const notePath = window.location.hash.substring(1);
|
||||
const noteId = notePath.split("/").at(-1);
|
||||
if (!noteId) return;
|
||||
|
||||
await import("./print.css");
|
||||
const froca = (await import("./services/froca")).default;
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
render(<App note={note} noteId={noteId} />, document.body);
|
||||
}
|
||||
|
||||
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
||||
const sentReadyEvent = useRef(false);
|
||||
const onReady = useCallback(() => {
|
||||
if (sentReadyEvent.current) return;
|
||||
window.dispatchEvent(new Event("note-ready"));
|
||||
window._noteReady = true;
|
||||
sentReadyEvent.current = true;
|
||||
}, []);
|
||||
const props: RendererProps | undefined | null = note && { note, onReady };
|
||||
|
||||
if (!note || !props) return <Error404 noteId={noteId} />
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.body.dataset.noteType = note.type;
|
||||
}, [ note ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{note.type === "book"
|
||||
? <CollectionRenderer {...props} />
|
||||
: <SingleNoteRenderer {...props} />
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
async function load() {
|
||||
if (note.type === "text") {
|
||||
await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
|
||||
}
|
||||
const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
|
||||
const container = containerRef.current!;
|
||||
container.replaceChildren(...$renderedContent);
|
||||
|
||||
// Wait for all images to load.
|
||||
const images = Array.from(container.querySelectorAll("img"));
|
||||
await Promise.all(
|
||||
images.map(img => {
|
||||
if (img.complete) return Promise.resolve();
|
||||
return new Promise<void>(resolve => {
|
||||
img.addEventListener("load", () => resolve(), { once: true });
|
||||
img.addEventListener("error", () => resolve(), { once: true });
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
load().then(() => requestAnimationFrame(onReady))
|
||||
}, [ note ]);
|
||||
|
||||
return <>
|
||||
<h1>{note.title}</h1>
|
||||
<main ref={containerRef} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function CollectionRenderer({ note, onReady }: RendererProps) {
|
||||
return <CustomNoteList
|
||||
isEnabled
|
||||
note={note}
|
||||
notePath={note.getBestNotePath().join("/")}
|
||||
ntxId="print"
|
||||
highlightedTokens={null}
|
||||
media="print"
|
||||
onReady={onReady}
|
||||
/>;
|
||||
}
|
||||
|
||||
function Error404({ noteId }: { noteId: string }) {
|
||||
return (
|
||||
<main>
|
||||
<p>The note you are trying to print could not be found.</p>
|
||||
<small>{noteId}</small>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,15 +1,5 @@
|
||||
import $ from "jquery";
|
||||
|
||||
async function loadBootstrap() {
|
||||
if (document.body.dir === "rtl") {
|
||||
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
|
||||
} else {
|
||||
await import("bootstrap/dist/css/bootstrap.min.css");
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).$ = $;
|
||||
(window as any).jQuery = $;
|
||||
await loadBootstrap();
|
||||
|
||||
$("body").show();
|
||||
|
||||
@@ -210,7 +210,7 @@ function makeToast(id: string, message: string): ToastOptions {
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async (message) => {
|
||||
if (!("taskType" in message) || message.taskType !== "deleteNotes") {
|
||||
if (message.taskType !== "deleteNotes") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ ws.subscribeToMessages(async (message) => {
|
||||
});
|
||||
|
||||
ws.subscribeToMessages(async (message) => {
|
||||
if (!("taskType" in message) || message.taskType !== "undeleteNotes") {
|
||||
if (message.taskType !== "undeleteNotes") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,11 @@ interface Options {
|
||||
tooltip?: boolean;
|
||||
trim?: boolean;
|
||||
imageHasZoom?: boolean;
|
||||
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
|
||||
noChildrenList?: boolean;
|
||||
}
|
||||
|
||||
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||
|
||||
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
||||
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
||||
|
||||
options = Object.assign(
|
||||
{
|
||||
@@ -44,7 +42,7 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
||||
const $renderedContent = $('<div class="rendered-content">');
|
||||
|
||||
if (type === "text" || type === "book") {
|
||||
await renderText(entity, $renderedContent, options);
|
||||
await renderText(entity, $renderedContent);
|
||||
} else if (type === "code") {
|
||||
await renderCode(entity, $renderedContent);
|
||||
} else if (["image", "canvas", "mindMap"].includes(type)) {
|
||||
@@ -116,7 +114,7 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
||||
};
|
||||
}
|
||||
|
||||
async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
|
||||
async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||
// entity must be FNote
|
||||
const blob = await note.getBlob();
|
||||
|
||||
@@ -137,7 +135,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
||||
}
|
||||
|
||||
await formatCodeBlocks($renderedContent);
|
||||
} else if (note instanceof FNote && !options.noChildrenList) {
|
||||
} else if (note instanceof FNote) {
|
||||
await renderChildrenList($renderedContent, note);
|
||||
}
|
||||
}
|
||||
@@ -258,19 +256,8 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
|
||||
</button>
|
||||
`);
|
||||
|
||||
$downloadButton.on("click", (e) => {
|
||||
e.stopPropagation();
|
||||
openService.downloadFileNote(entity.noteId)
|
||||
});
|
||||
$openButton.on("click", async (e) => {
|
||||
const iconEl = $openButton.find("> .bx");
|
||||
iconEl.removeClass("bx bx-link-external");
|
||||
iconEl.addClass("bx bx-loader spin");
|
||||
e.stopPropagation();
|
||||
await openService.openNoteExternally(entity.noteId, entity.mime)
|
||||
iconEl.removeClass("bx bx-loader spin");
|
||||
iconEl.addClass("bx bx-link-external");
|
||||
});
|
||||
$downloadButton.on("click", () => openService.downloadFileNote(entity.noteId));
|
||||
$openButton.on("click", () => openService.openNoteExternally(entity.noteId, entity.mime));
|
||||
// open doesn't work for protected notes since it works through a browser which isn't in protected session
|
||||
$openButton.toggle(!entity.isProtected);
|
||||
|
||||
|
||||
@@ -1,39 +1,21 @@
|
||||
import {readCssVar} from "../utils/css-var";
|
||||
import Color, { ColorInstance } from "color";
|
||||
|
||||
const registeredClasses = new Set<string>();
|
||||
|
||||
// Read the color lightness limits defined in the theme as CSS variables
|
||||
function createClassForColor(color: string | null) {
|
||||
if (!color?.trim()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const lightThemeColorMaxLightness = readCssVar(
|
||||
document.documentElement,
|
||||
"tree-item-light-theme-max-color-lightness"
|
||||
).asNumber(70);
|
||||
const normalizedColorName = color.replace(/[^a-z0-9]/gi, "");
|
||||
|
||||
const darkThemeColorMinLightness = readCssVar(
|
||||
document.documentElement,
|
||||
"tree-item-dark-theme-min-color-lightness"
|
||||
).asNumber(50);
|
||||
if (!normalizedColorName.trim()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function createClassForColor(colorString: string | null) {
|
||||
if (!colorString?.trim()) return "";
|
||||
|
||||
const color = parseColor(colorString);
|
||||
if (!color) return "";
|
||||
|
||||
const className = `color-${color.hex().substring(1)}`;
|
||||
const className = `color-${normalizedColorName}`;
|
||||
|
||||
if (!registeredClasses.has(className)) {
|
||||
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
|
||||
darkThemeColorMinLightness!);
|
||||
|
||||
$("head").append(`<style>
|
||||
.${className}, span.fancytree-active.${className} {
|
||||
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
|
||||
--dark-theme-custom-color: ${adjustedColor.darkThemeColor};
|
||||
--custom-color-hue: ${getHue(color) ?? 'unset'};
|
||||
}
|
||||
</style>`);
|
||||
// make the active fancytree selector more specific than the normal color setting
|
||||
$("head").append(`<style>.${className}, span.fancytree-active.${className} { color: ${color} !important; }</style>`);
|
||||
|
||||
registeredClasses.add(className);
|
||||
}
|
||||
@@ -41,41 +23,6 @@ function createClassForColor(colorString: string | null) {
|
||||
return className;
|
||||
}
|
||||
|
||||
function parseColor(color: string) {
|
||||
try {
|
||||
return Color(color);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pair of colors — one optimized for light themes and the other for dark themes, derived
|
||||
* from the specified color to maintain sufficient contrast with each theme.
|
||||
* The adjustment is performed by limiting the color’s lightness in the CIELAB color space,
|
||||
* according to the lightThemeMaxLightness and darkThemeMinLightness parameters.
|
||||
*/
|
||||
function adjustColorLightness(color: ColorInstance, lightThemeMaxLightness: number, darkThemeMinLightness: number) {
|
||||
const labColor = color.lab();
|
||||
const lightness = labColor.l();
|
||||
|
||||
// For the light theme, limit the maximum lightness
|
||||
const lightThemeColor = labColor.l(Math.min(lightness, lightThemeMaxLightness)).hex();
|
||||
|
||||
// For the dark theme, limit the minimum lightness
|
||||
const darkThemeColor = labColor.l(Math.max(lightness, darkThemeMinLightness)).hex();
|
||||
|
||||
return {lightThemeColor, darkThemeColor};
|
||||
}
|
||||
|
||||
/** Returns the hue of the specified color, or undefined if the color is grayscale. */
|
||||
function getHue(color: ColorInstance) {
|
||||
const hslColor = color.hsl();
|
||||
if (hslColor.saturationl() > 0) {
|
||||
return hslColor.hue();
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
createClassForColor
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ async function confirmDeleteNoteBoxWithNote(title: string) {
|
||||
return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
|
||||
}
|
||||
|
||||
export async function prompt(props: PromptDialogOptions) {
|
||||
async function prompt(props: PromptDialogOptions) {
|
||||
return new Promise<string | null>((res) => appContext.triggerCommand("showPromptDialog", { ...props, callback: res }));
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,6 @@ function getUrl(docNameValue: string, language: string) {
|
||||
// Cannot have spaces in the URL due to how JQuery.load works.
|
||||
docNameValue = docNameValue.replaceAll(" ", "%20");
|
||||
|
||||
const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath;
|
||||
const basePath = window.glob.isDev ? new URL(window.glob.assetPath).pathname : window.glob.assetPath;
|
||||
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { OpenedFileUpdateStatus } from "@triliumnext/commons";
|
||||
|
||||
const fileModificationStatus: Record<string, Record<string, OpenedFileUpdateStatus>> = {
|
||||
// TODO: Deduplicate
|
||||
interface Message {
|
||||
type: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
lastModifiedMs: number;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const fileModificationStatus: Record<string, Record<string, Message>> = {
|
||||
notes: {},
|
||||
attachments: {}
|
||||
};
|
||||
@@ -31,7 +39,7 @@ function ignoreModification(entityType: string, entityId: string) {
|
||||
delete fileModificationStatus[entityType][entityId];
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async message => {
|
||||
ws.subscribeToMessages(async (message: Message) => {
|
||||
if (message.type !== "openedFileUpdated") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,23 +40,20 @@ class FrocaImpl implements Froca {
|
||||
|
||||
constructor() {
|
||||
this.initializedPromise = this.loadInitialTree();
|
||||
this.#clear();
|
||||
}
|
||||
|
||||
async loadInitialTree() {
|
||||
const resp = await server.get<SubtreeResponse>("tree");
|
||||
|
||||
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
|
||||
this.#clear();
|
||||
this.addResp(resp);
|
||||
}
|
||||
|
||||
#clear() {
|
||||
this.notes = {};
|
||||
this.branches = {};
|
||||
this.attributes = {};
|
||||
this.attachments = {};
|
||||
this.blobPromises = {};
|
||||
|
||||
this.addResp(resp);
|
||||
}
|
||||
|
||||
async loadSubTree(subTreeNoteId: string) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
|
||||
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
|
||||
import type { default as FNote, FNoteRow } from "../entities/fnote.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
import type { OptionNames } from "@triliumnext/commons";
|
||||
|
||||
async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
const loadResults = new LoadResults(entityChanges);
|
||||
@@ -31,8 +30,9 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
continue; // only noise
|
||||
}
|
||||
|
||||
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
|
||||
loadResults.addOption(attributeEntity.name as OptionNames);
|
||||
options.set(attributeEntity.name, attributeEntity.value);
|
||||
|
||||
loadResults.addOption(attributeEntity.name);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
processAttachment(loadResults, ec);
|
||||
} else if (ec.entityName === "blobs") {
|
||||
|
||||
@@ -21,7 +21,6 @@ import dayjs from "dayjs";
|
||||
import type NoteContext from "../components/note_context.js";
|
||||
import type NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import type Component from "../components/component.js";
|
||||
import { formatLogMessage } from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* A whole number
|
||||
@@ -456,7 +455,7 @@ export interface Api {
|
||||
/**
|
||||
* Log given message to the log pane in UI
|
||||
*/
|
||||
log(message: string | object): void;
|
||||
log(message: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,7 +696,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
|
||||
this.log = (message) => {
|
||||
const { noteId } = this.startNote;
|
||||
|
||||
message = `${utils.now()}: ${formatLogMessage(message)}`;
|
||||
message = `${utils.now()}: ${message}`;
|
||||
|
||||
console.log(`Script ${noteId}: ${message}`);
|
||||
|
||||
|
||||
28
apps/client/src/services/frontend_script_entrypoint.ts
Normal file
28
apps/client/src/services/frontend_script_entrypoint.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* The front script API is accessible to code notes with the "JS (frontend)" language.
|
||||
*
|
||||
* The entire API is exposed as a single global: {@link api}
|
||||
*
|
||||
* @module Frontend Script API
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
||||
* script note.
|
||||
*
|
||||
* Make sure to keep in line with frontend's `script_context.ts`.
|
||||
*/
|
||||
|
||||
export type { default as BasicWidget } from "../widgets/basic_widget.js";
|
||||
export type { default as FAttachment } from "../entities/fattachment.js";
|
||||
export type { default as FAttribute } from "../entities/fattribute.js";
|
||||
export type { default as FBranch } from "../entities/fbranch.js";
|
||||
export type { default as FNote } from "../entities/fnote.js";
|
||||
export type { Api } from "./frontend_script_api.js";
|
||||
export type { default as NoteContextAwareWidget } from "../widgets/note_context_aware_widget.js";
|
||||
export type { default as RightPanelWidget } from "../widgets/right_panel_widget.js";
|
||||
|
||||
import FrontendScriptApi, { type Api } from "./frontend_script_api.js";
|
||||
|
||||
//@ts-expect-error
|
||||
export const api: Api = new FrontendScriptApi();
|
||||
@@ -20,6 +20,9 @@ function setupGlobs() {
|
||||
window.glob.froca = froca;
|
||||
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
|
||||
|
||||
// for CKEditor integration (button on block toolbar)
|
||||
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
const string = String(msg).toLowerCase();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
|
||||
describe("i18n", () => {
|
||||
it("translations are valid JSON", () => {
|
||||
for (const locale of LOCALES) {
|
||||
if (locale.contentOnly || locale.id === "en_rtl") {
|
||||
if (locale.contentOnly) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import ws from "./ws.js";
|
||||
import utils from "./utils.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
|
||||
type BooleanLike = boolean | "true" | "false";
|
||||
|
||||
@@ -67,7 +66,7 @@ function makeToast(id: string, message: string): ToastOptions {
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async (message) => {
|
||||
if (!("taskType" in message) || message.taskType !== "importNotes") {
|
||||
if (message.taskType !== "importNotes") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,8 +87,8 @@ ws.subscribeToMessages(async (message) => {
|
||||
}
|
||||
});
|
||||
|
||||
ws.subscribeToMessages(async (message: WebSocketMessage) => {
|
||||
if (!("taskType" in message) || message.taskType !== "importAttachments") {
|
||||
ws.subscribeToMessages(async (message) => {
|
||||
if (message.taskType !== "importAttachments") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { ViewTypeOptions } from "./note_list_renderer";
|
||||
import FNote from "../entities/fnote";
|
||||
import { ViewTypeOptions } from "../widgets/collections/interface";
|
||||
|
||||
export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
canvas: null,
|
||||
@@ -10,7 +10,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
file: null,
|
||||
image: null,
|
||||
launcher: null,
|
||||
mermaid: "s1aBHPd79XYj",
|
||||
mermaid: null,
|
||||
mindMap: null,
|
||||
noteMap: null,
|
||||
relationMap: null,
|
||||
@@ -27,8 +27,7 @@ export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
calendar: "xWbu3jpNWapp",
|
||||
table: "2FvYrpmOXm29",
|
||||
geoMap: "81SGnPGMk7Xc",
|
||||
board: "CtBQqbwXDx1w",
|
||||
presentation: null
|
||||
board: "CtBQqbwXDx1w"
|
||||
};
|
||||
|
||||
export function getHelpUrlForNote(note: FNote | null | undefined) {
|
||||
|
||||
@@ -3,8 +3,16 @@ import linkContextMenuService from "../menus/link_context_menu.js";
|
||||
import appContext, { type NoteCommandData } from "../components/app_context.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
|
||||
import { openInCurrentNoteContext } from "../components/note_context.js";
|
||||
|
||||
// Be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts`
|
||||
// TODO: Deduplicate with server once we can.
|
||||
export const ALLOWED_PROTOCOLS = [
|
||||
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
|
||||
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
|
||||
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
|
||||
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
|
||||
'mid'
|
||||
];
|
||||
|
||||
function getNotePathFromUrl(url: string) {
|
||||
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
|
||||
@@ -27,7 +35,8 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
export type ViewMode = "default" | "source" | "attachments" | "contextual-help";
|
||||
// TODO: Remove `string` once all the view modes have been mapped.
|
||||
type ViewMode = "default" | "source" | "attachments" | "contextual-help" | string;
|
||||
|
||||
export interface ViewScope {
|
||||
/**
|
||||
@@ -317,7 +326,21 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
||||
viewScope
|
||||
});
|
||||
} else if (isLeftClick) {
|
||||
openInCurrentNoteContext(evt, notePath, viewScope);
|
||||
const ntxId = $(evt?.target as any)
|
||||
.closest("[data-ntx-id]")
|
||||
.attr("data-ntx-id");
|
||||
|
||||
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
|
||||
|
||||
if (noteContext) {
|
||||
noteContext.setNote(notePath, { viewScope }).then(() => {
|
||||
if (noteContext !== appContext.tabManager.getActiveContext()) {
|
||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
|
||||
}
|
||||
}
|
||||
} else if (hrefLink) {
|
||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AttachmentRow, EtapiTokenRow, OptionNames } from "@triliumnext/commons";
|
||||
import type { AttachmentRow, EtapiTokenRow } from "@triliumnext/commons";
|
||||
import type { AttributeType } from "../entities/fattribute.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
|
||||
@@ -67,7 +67,7 @@ export default class LoadResults {
|
||||
private revisionRows: RevisionRow[];
|
||||
private noteReorderings: string[];
|
||||
private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
|
||||
private optionNames: OptionNames[];
|
||||
private optionNames: string[];
|
||||
private attachmentRows: AttachmentRow[];
|
||||
public hasEtapiTokenChanges: boolean = false;
|
||||
|
||||
@@ -180,11 +180,11 @@ export default class LoadResults {
|
||||
return this.contentNoteIdToComponentId.find((l) => l.noteId === noteId && l.componentId !== componentId);
|
||||
}
|
||||
|
||||
addOption(name: OptionNames) {
|
||||
addOption(name: string) {
|
||||
this.optionNames.push(name);
|
||||
}
|
||||
|
||||
isOptionReloaded(name: OptionNames) {
|
||||
isOptionReloaded(name: string) {
|
||||
return this.optionNames.includes(name);
|
||||
}
|
||||
|
||||
|
||||
71
apps/client/src/services/note_list_renderer.ts
Normal file
71
apps/client/src/services/note_list_renderer.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import BoardView from "../widgets/view_widgets/board_view/index.js";
|
||||
import CalendarView from "../widgets/view_widgets/calendar_view.js";
|
||||
import GeoView from "../widgets/view_widgets/geo_view/index.js";
|
||||
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
|
||||
import TableView from "../widgets/view_widgets/table_view/index.js";
|
||||
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
|
||||
import type ViewMode from "../widgets/view_widgets/view_mode.js";
|
||||
|
||||
const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board"] as const;
|
||||
export type ArgsWithoutNoteId = Omit<ViewModeArgs, "noteIds">;
|
||||
export type ViewTypeOptions = typeof allViewTypes[number];
|
||||
|
||||
export default class NoteListRenderer {
|
||||
|
||||
private viewType: ViewTypeOptions;
|
||||
private args: ArgsWithoutNoteId;
|
||||
public viewMode?: ViewMode<any>;
|
||||
|
||||
constructor(args: ArgsWithoutNoteId) {
|
||||
this.args = args;
|
||||
this.viewType = this.#getViewType(args.parentNote);
|
||||
}
|
||||
|
||||
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||
const viewType = parentNote.getLabelValue("viewType");
|
||||
|
||||
if (!(allViewTypes as readonly string[]).includes(viewType || "")) {
|
||||
// when not explicitly set, decide based on the note type
|
||||
return parentNote.type === "search" ? "list" : "grid";
|
||||
} else {
|
||||
return viewType as ViewTypeOptions;
|
||||
}
|
||||
}
|
||||
|
||||
get isFullHeight() {
|
||||
switch (this.viewType) {
|
||||
case "list":
|
||||
case "grid":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
const args = this.args;
|
||||
const viewMode = this.#buildViewMode(args);
|
||||
this.viewMode = viewMode;
|
||||
await viewMode.beforeRender();
|
||||
return await viewMode.renderList();
|
||||
}
|
||||
|
||||
#buildViewMode(args: ViewModeArgs) {
|
||||
switch (this.viewType) {
|
||||
case "calendar":
|
||||
return new CalendarView(args);
|
||||
case "table":
|
||||
return new TableView(args);
|
||||
case "geoMap":
|
||||
return new GeoView(args);
|
||||
case "board":
|
||||
return new BoardView(args);
|
||||
case "list":
|
||||
case "grid":
|
||||
default:
|
||||
return new ListOrGridView(this.viewType, args);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from "./i18n.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
|
||||
import type { MenuCommandItem, MenuItem, MenuItemBadge } from "../menus/context_menu.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
|
||||
|
||||
@@ -73,7 +73,7 @@ const BETA_BADGE = {
|
||||
title: t("note_types.beta-feature")
|
||||
};
|
||||
|
||||
const SEPARATOR: MenuSeparatorItem = { kind: "separator" };
|
||||
const SEPARATOR = { title: "----" };
|
||||
|
||||
const creationDateCache = new Map<string, Date>();
|
||||
let rootCreationDate: Date | undefined;
|
||||
@@ -81,8 +81,8 @@ let rootCreationDate: Date | undefined;
|
||||
async function getNoteTypeItems(command?: TreeCommandNames) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
...getBlankNoteTypes(command),
|
||||
...await getBuiltInTemplates(null, command, false),
|
||||
...await getBuiltInTemplates(t("note_types.collections"), command, true),
|
||||
...await getBuiltInTemplates(null, command, false),
|
||||
...await getUserTemplates(command)
|
||||
];
|
||||
|
||||
@@ -121,10 +121,7 @@ async function getUserTemplates(command?: TreeCommandNames) {
|
||||
}
|
||||
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
{
|
||||
title: t("note_type_chooser.templates"),
|
||||
kind: "header"
|
||||
}
|
||||
SEPARATOR
|
||||
];
|
||||
|
||||
for (const templateNote of templateNotes) {
|
||||
@@ -161,15 +158,15 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
|
||||
if (title) {
|
||||
items.push({
|
||||
title: title,
|
||||
kind: "header"
|
||||
enabled: false,
|
||||
uiIcon: "bx bx-empty"
|
||||
});
|
||||
} else {
|
||||
items.push(SEPARATOR);
|
||||
}
|
||||
|
||||
for (const templateNote of childNotes) {
|
||||
if (templateNote.hasLabel("collection") !== filterCollections ||
|
||||
!templateNote.hasLabel("template")) {
|
||||
if (templateNote.hasLabel("collection") !== filterCollections) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class Options {
|
||||
this.arr = arr;
|
||||
}
|
||||
|
||||
get(key: OptionNames) {
|
||||
get(key: string) {
|
||||
return this.arr?.[key] as string;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Options {
|
||||
}
|
||||
}
|
||||
|
||||
getInt(key: OptionNames) {
|
||||
getInt(key: string) {
|
||||
const value = this.arr?.[key];
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
@@ -52,7 +52,7 @@ class Options {
|
||||
return null;
|
||||
}
|
||||
|
||||
getFloat(key: OptionNames) {
|
||||
getFloat(key: string) {
|
||||
const value = this.arr?.[key];
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
@@ -60,15 +60,15 @@ class Options {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
is(key: OptionNames) {
|
||||
is(key: string) {
|
||||
return this.arr[key] === "true";
|
||||
}
|
||||
|
||||
set(key: OptionNames, value: OptionValue) {
|
||||
set(key: string, value: OptionValue) {
|
||||
this.arr[key] = value;
|
||||
}
|
||||
|
||||
async save(key: OptionNames, value: OptionValue) {
|
||||
async save(key: string, value: OptionValue) {
|
||||
this.set(key, value);
|
||||
|
||||
const payload: Record<string, OptionValue> = {};
|
||||
@@ -85,7 +85,7 @@ class Options {
|
||||
await server.put<void>("options", newValues);
|
||||
}
|
||||
|
||||
async toggle(key: OptionNames) {
|
||||
async toggle(key: string) {
|
||||
await this.save(key, (!this.is(key)).toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,11 +107,11 @@ function makeToast(message: Message, title: string, text: string): ToastOptions
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async (message) => {
|
||||
if (!("taskType" in message) || message.taskType !== "protectNotes") {
|
||||
if (message.taskType !== "protectNotes") {
|
||||
return;
|
||||
}
|
||||
|
||||
const isProtecting = message.data?.protect;
|
||||
const isProtecting = message.data.protect;
|
||||
const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title");
|
||||
|
||||
if (message.type === "taskError") {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import options from "./options.js";
|
||||
import Split from "@triliumnext/split.js";
|
||||
import Split from "split.js"
|
||||
|
||||
export const DEFAULT_GUTTER_SIZE = 5;
|
||||
|
||||
@@ -10,10 +10,6 @@ let leftInstance: ReturnType<typeof Split> | null;
|
||||
let rightPaneWidth: number;
|
||||
let rightInstance: ReturnType<typeof Split> | null;
|
||||
|
||||
const noteSplitMap = new Map<string[], ReturnType<typeof Split> | undefined>(); // key: a group of ntxIds, value: the corresponding Split instance
|
||||
const noteSplitRafMap = new Map<string[], number>();
|
||||
let splitNoteContainer: HTMLElement | undefined;
|
||||
|
||||
function setupLeftPaneResizer(leftPaneVisible: boolean) {
|
||||
if (leftInstance) {
|
||||
leftInstance.destroy();
|
||||
@@ -46,7 +42,6 @@ function setupLeftPaneResizer(leftPaneVisible: boolean) {
|
||||
sizes: [leftPaneWidth, restPaneWidth],
|
||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||
minSize: [150, 300],
|
||||
rtl: glob.isRtl,
|
||||
onDragEnd: (sizes) => {
|
||||
leftPaneWidth = Math.round(sizes[0]);
|
||||
options.save("leftPaneWidth", Math.round(sizes[0]));
|
||||
@@ -80,7 +75,6 @@ function setupRightPaneResizer() {
|
||||
sizes: [100 - rightPaneWidth, rightPaneWidth],
|
||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||
minSize: [300, 180],
|
||||
rtl: glob.isRtl,
|
||||
onDragEnd: (sizes) => {
|
||||
rightPaneWidth = Math.round(sizes[1]);
|
||||
options.save("rightPaneWidth", Math.round(sizes[1]));
|
||||
@@ -89,87 +83,7 @@ function setupRightPaneResizer() {
|
||||
}
|
||||
}
|
||||
|
||||
function findKeyByNtxId(ntxId: string): string[] | undefined {
|
||||
// Find the corresponding key in noteSplitMap based on ntxId
|
||||
for (const key of noteSplitMap.keys()) {
|
||||
if (key.includes(ntxId)) return key;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setupNoteSplitResizer(ntxIds: string[]) {
|
||||
let targetNtxIds: string[] | undefined;
|
||||
for (const ntxId of ntxIds) {
|
||||
targetNtxIds = findKeyByNtxId(ntxId);
|
||||
if (targetNtxIds) break;
|
||||
}
|
||||
|
||||
if (targetNtxIds) {
|
||||
noteSplitMap.get(targetNtxIds)?.destroy();
|
||||
for (const id of ntxIds) {
|
||||
if (!targetNtxIds.includes(id)) {
|
||||
targetNtxIds.push(id)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
targetNtxIds = [...ntxIds];
|
||||
}
|
||||
noteSplitMap.set(targetNtxIds, undefined);
|
||||
createSplitInstance(targetNtxIds);
|
||||
}
|
||||
|
||||
|
||||
function delNoteSplitResizer(ntxIds: string[]) {
|
||||
let targetNtxIds = findKeyByNtxId(ntxIds[0]);
|
||||
if (!targetNtxIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
noteSplitMap.get(targetNtxIds)?.destroy();
|
||||
noteSplitMap.delete(targetNtxIds);
|
||||
targetNtxIds = targetNtxIds.filter(id => !ntxIds.includes(id));
|
||||
|
||||
if (targetNtxIds.length >= 2) {
|
||||
noteSplitMap.set(targetNtxIds, undefined);
|
||||
createSplitInstance(targetNtxIds);
|
||||
}
|
||||
}
|
||||
|
||||
function moveNoteSplitResizer(ntxId: string) {
|
||||
const targetNtxIds = findKeyByNtxId(ntxId);
|
||||
if (!targetNtxIds) {
|
||||
return;
|
||||
}
|
||||
noteSplitMap.get(targetNtxIds)?.destroy();
|
||||
noteSplitMap.set(targetNtxIds, undefined);
|
||||
createSplitInstance(targetNtxIds);
|
||||
}
|
||||
|
||||
function createSplitInstance(targetNtxIds: string[]) {
|
||||
const prevRafId = noteSplitRafMap.get(targetNtxIds);
|
||||
if (prevRafId) {
|
||||
cancelAnimationFrame(prevRafId);
|
||||
}
|
||||
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
splitNoteContainer = splitNoteContainer ?? $("#center-pane").find(".split-note-container-widget")[0];
|
||||
const splitPanels = [...splitNoteContainer.querySelectorAll<HTMLElement>(':scope > .note-split')]
|
||||
.filter(el => targetNtxIds.includes(el.getAttribute('data-ntx-id') ?? ""));
|
||||
const splitInstance = Split(splitPanels, {
|
||||
rtl: glob.isRtl,
|
||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||
minSize: 150,
|
||||
});
|
||||
noteSplitMap.set(targetNtxIds, splitInstance);
|
||||
noteSplitRafMap.delete(targetNtxIds);
|
||||
});
|
||||
noteSplitRafMap.set(targetNtxIds, rafId);
|
||||
}
|
||||
|
||||
export default {
|
||||
setupLeftPaneResizer,
|
||||
setupRightPaneResizer,
|
||||
setupNoteSplitResizer,
|
||||
delNoteSplitResizer,
|
||||
moveNoteSplitResizer
|
||||
setupRightPaneResizer
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
|
||||
import shortcuts, { keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
|
||||
// Mock utils module
|
||||
vi.mock("./utils.js", () => ({
|
||||
@@ -119,6 +119,11 @@ describe("shortcuts", () => {
|
||||
metaKey: options.metaKey || false
|
||||
} as KeyboardEvent);
|
||||
|
||||
it("should match simple key shortcuts", () => {
|
||||
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
||||
expect(matchesShortcut(event, "a")).toBe(true);
|
||||
});
|
||||
|
||||
it("should match shortcuts with modifiers", () => {
|
||||
const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
||||
expect(matchesShortcut(event, "ctrl+a")).toBe(true);
|
||||
@@ -143,28 +148,6 @@ describe("shortcuts", () => {
|
||||
expect(matchesShortcut(event, "a")).toBe(false);
|
||||
});
|
||||
|
||||
it("should not match when no modifiers are used", () => {
|
||||
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
||||
expect(matchesShortcut(event, "a")).toBe(false);
|
||||
});
|
||||
|
||||
it("should match some keys even with no modifiers", () => {
|
||||
// Bare function keys
|
||||
let event = createKeyboardEvent({ key: "F1", code: "F1" });
|
||||
expect(matchesShortcut(event, "F1")).toBeTruthy();
|
||||
expect(matchesShortcut(event, "f1")).toBeTruthy();
|
||||
|
||||
// Function keys with shift
|
||||
event = createKeyboardEvent({ key: "F1", code: "F1", shiftKey: true });
|
||||
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
||||
|
||||
// Special keys
|
||||
for (const keyCode of [ "Delete", "Enter" ]) {
|
||||
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
||||
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle alternative modifier names", () => {
|
||||
const ctrlEvent = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
||||
expect(matchesShortcut(ctrlEvent, "control+a")).toBe(true);
|
||||
@@ -337,36 +320,4 @@ describe("shortcuts", () => {
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isIMEComposing', () => {
|
||||
it('should return true when event.isComposing is true', () => {
|
||||
const event = { isComposing: true, keyCode: 65 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when keyCode is 229', () => {
|
||||
const event = { isComposing: false, keyCode: 229 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when both isComposing is true and keyCode is 229', () => {
|
||||
const event = { isComposing: true, keyCode: 229 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for normal keys', () => {
|
||||
const event = { isComposing: false, keyCode: 65 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when isComposing is undefined and keyCode is not 229', () => {
|
||||
const event = { keyCode: 13 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null/undefined events gracefully', () => {
|
||||
expect(isIMEComposing(null as any)).toBe(false);
|
||||
expect(isIMEComposing(undefined as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,35 +36,8 @@ const keyMap: { [key: string]: string[] } = {
|
||||
};
|
||||
|
||||
// Function keys
|
||||
const functionKeyCodes: string[] = [];
|
||||
for (let i = 1; i <= 19; i++) {
|
||||
const keyCode = `F${i}`;
|
||||
functionKeyCodes.push(keyCode);
|
||||
keyMap[`f${i}`] = [ keyCode ];
|
||||
}
|
||||
|
||||
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
||||
"Delete",
|
||||
"Enter",
|
||||
...functionKeyCodes
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if IME (Input Method Editor) is composing
|
||||
* This is used to prevent keyboard shortcuts from firing during IME composition
|
||||
* @param e - The keyboard event to check
|
||||
* @returns true if IME is currently composing, false otherwise
|
||||
*/
|
||||
export function isIMEComposing(e: KeyboardEvent): boolean {
|
||||
// Handle null/undefined events gracefully
|
||||
if (!e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Standard check for composition state
|
||||
// e.isComposing is true when IME is actively composing
|
||||
// e.keyCode === 229 is a fallback for older browsers where 229 indicates IME processing
|
||||
return e.isComposing || e.keyCode === 229;
|
||||
keyMap[`f${i}`] = [`F${i}`];
|
||||
}
|
||||
|
||||
function removeGlobalShortcut(namespace: string) {
|
||||
@@ -95,13 +68,6 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
|
||||
}
|
||||
|
||||
const e = evt as KeyboardEvent;
|
||||
|
||||
// Skip processing if IME is composing to prevent shortcuts from
|
||||
// interfering with text input in CJK languages
|
||||
if (isIMEComposing(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesShortcut(e, keyboardShortcut)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -171,12 +137,6 @@ export function matchesShortcut(e: KeyboardEvent, shortcut: string): boolean {
|
||||
const expectedShift = modifiers.includes('shift');
|
||||
const expectedMeta = modifiers.includes('meta') || modifiers.includes('cmd') || modifiers.includes('command');
|
||||
|
||||
// Refuse key combinations that don't include modifiers because they interfere with the normal usage of the application.
|
||||
// Some keys such as function keys are an exception.
|
||||
if (!(expectedCtrl || expectedAlt || expectedShift || expectedMeta) && !KEYCODES_WITH_NO_MODIFIER.has(e.code)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.ctrlKey === expectedCtrl &&
|
||||
e.altKey === expectedAlt &&
|
||||
e.shiftKey === expectedShift &&
|
||||
|
||||
@@ -61,11 +61,7 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
|
||||
highlightedText = highlightAuto(text);
|
||||
} else if (normalizedMimeType) {
|
||||
await ensureMimeTypesForHighlighting(normalizedMimeType);
|
||||
try {
|
||||
highlightedText = highlight(text, { language: normalizedMimeType });
|
||||
} catch (e) {
|
||||
console.warn("Unable to apply syntax highlight.", e);
|
||||
}
|
||||
highlightedText = highlight(text, { language: normalizedMimeType });
|
||||
}
|
||||
|
||||
if (highlightedText) {
|
||||
@@ -80,7 +76,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
|
||||
// Load theme.
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
loadHighlightingTheme(currentThemeName);
|
||||
|
||||
// Load mime types.
|
||||
let mimeTypes: MimeType[];
|
||||
@@ -102,16 +98,17 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
highlightingLoaded = true;
|
||||
}
|
||||
|
||||
export async function loadHighlightingTheme(themeName: string) {
|
||||
export function loadHighlightingTheme(themeName: string) {
|
||||
const themePrefix = "default:";
|
||||
let theme: Theme | null = null;
|
||||
if (glob.device === "print") {
|
||||
theme = Themes.vs;
|
||||
} else if (themeName.includes(themePrefix)) {
|
||||
if (themeName.includes(themePrefix)) {
|
||||
theme = Themes[themeName.substring(themePrefix.length)];
|
||||
}
|
||||
if (!theme) {
|
||||
theme = Themes.default;
|
||||
}
|
||||
|
||||
await loadTheme(theme ?? Themes.default);
|
||||
loadTheme(theme);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ws from "./ws.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
export interface ToastOptions {
|
||||
id?: string;
|
||||
icon: string;
|
||||
title?: string;
|
||||
title: string;
|
||||
message: string;
|
||||
delay?: number;
|
||||
autohide?: boolean;
|
||||
@@ -11,32 +12,20 @@ export interface ToastOptions {
|
||||
}
|
||||
|
||||
function toast(options: ToastOptions) {
|
||||
const $toast = $(options.title
|
||||
? `\
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">
|
||||
<span class="bx bx-${options.icon}"></span>
|
||||
<span class="toast-title"></span>
|
||||
</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body"></div>
|
||||
</div>`
|
||||
: `
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-icon">
|
||||
const $toast = $(
|
||||
`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">
|
||||
<span class="bx bx-${options.icon}"></span>
|
||||
</div>
|
||||
<div class="toast-body"></div>
|
||||
<div class="toast-header">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>`
|
||||
<span class="toast-title"></span>
|
||||
</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body"></div>
|
||||
</div>`
|
||||
);
|
||||
|
||||
$toast.toggleClass("no-title", !options.title);
|
||||
$toast.find(".toast-title").text(options.title ?? "");
|
||||
$toast.find(".toast-title").text(options.title);
|
||||
$toast.find(".toast-body").html(options.message);
|
||||
|
||||
if (options.id) {
|
||||
@@ -81,6 +70,7 @@ function showMessage(message: string, delay = 2000) {
|
||||
console.debug(utils.now(), "message:", message);
|
||||
|
||||
toast({
|
||||
title: "Info",
|
||||
icon: "check",
|
||||
message: message,
|
||||
autohide: true,
|
||||
@@ -92,6 +82,7 @@ export function showError(message: string, delay = 10000) {
|
||||
console.log(utils.now(), "error: ", message);
|
||||
|
||||
toast({
|
||||
title: "Error",
|
||||
icon: "alert",
|
||||
message: message,
|
||||
autohide: true,
|
||||
|
||||
@@ -4,6 +4,9 @@ import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
|
||||
/**
|
||||
* @returns {string|null}
|
||||
*/
|
||||
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
||||
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||
|
||||
@@ -26,12 +29,21 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
||||
}
|
||||
|
||||
const path = notePath.split("/").reverse();
|
||||
|
||||
if (!path.includes("root")) {
|
||||
path.push("root");
|
||||
}
|
||||
|
||||
const effectivePathSegments: string[] = [];
|
||||
let childNoteId: string | null = null;
|
||||
let i = 0;
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const parentNoteId = path[i];
|
||||
while (true) {
|
||||
if (i >= path.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const parentNoteId = path[i++];
|
||||
|
||||
if (childNoteId !== null) {
|
||||
const child = await froca.getNote(childNoteId, !logErrors);
|
||||
@@ -56,7 +68,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parents.some(p => p.noteId === parentNoteId) || (i === path.length - 1 && parentNoteId !== 'root')) {
|
||||
if (!parents.some((p) => p.noteId === parentNoteId)) {
|
||||
if (logErrors) {
|
||||
const parent = froca.getNoteFromCache(parentNoteId);
|
||||
|
||||
@@ -68,8 +80,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
||||
);
|
||||
}
|
||||
|
||||
const activeNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||
const bestNotePath = child.getBestNotePath(hoistedNoteId, activeNotePath);
|
||||
const bestNotePath = child.getBestNotePath(hoistedNoteId);
|
||||
|
||||
if (bestNotePath) {
|
||||
const pathToRoot = bestNotePath.reverse().slice(1);
|
||||
@@ -100,9 +111,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
||||
if (!note) {
|
||||
throw new Error(`Unable to find note: ${notePath}.`);
|
||||
}
|
||||
|
||||
const activeNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||
const bestNotePath = note.getBestNotePath(hoistedNoteId, activeNotePath);
|
||||
const bestNotePath = note.getBestNotePath(hoistedNoteId);
|
||||
|
||||
if (!bestNotePath) {
|
||||
throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);
|
||||
|
||||
@@ -11,11 +11,7 @@ export function reloadFrontendApp(reason?: string) {
|
||||
logInfo(`Frontend app reload: ${reason}`);
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow()?.reload();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export function restartDesktopApp() {
|
||||
@@ -51,6 +47,27 @@ function parseDate(str: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/a/30465299/4898894
|
||||
function getMonthsInDateRange(startDate: string, endDate: string) {
|
||||
const start = startDate.split("-");
|
||||
const end = endDate.split("-");
|
||||
const startYear = parseInt(start[0]);
|
||||
const endYear = parseInt(end[0]);
|
||||
const dates: string[] = [];
|
||||
|
||||
for (let i = startYear; i <= endYear; i++) {
|
||||
const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1;
|
||||
const startMon = i === startYear ? parseInt(start[1]) - 1 : 0;
|
||||
|
||||
for (let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j + 1) {
|
||||
const month = j + 1;
|
||||
const displayMonth = month < 10 ? "0" + month : month;
|
||||
dates.push([i, displayMonth].join("-"));
|
||||
}
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
function padNum(num: number) {
|
||||
return `${num <= 9 ? "0" : ""}${num}`;
|
||||
}
|
||||
@@ -132,18 +149,6 @@ export function isElectron() {
|
||||
return !!(window && window.process && window.process.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the client is running as a PWA, otherwise `false`.
|
||||
*/
|
||||
export function isPWA() {
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.matchMedia('(display-mode: window-controls-overlay)').matches
|
||||
|| window.navigator.standalone
|
||||
|| window.navigator.windowControlsOverlay
|
||||
);
|
||||
}
|
||||
|
||||
export function isMac() {
|
||||
return navigator.platform.indexOf("Mac") > -1;
|
||||
}
|
||||
@@ -292,54 +297,6 @@ function isHtmlEmpty(html: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatHtml(html: string) {
|
||||
let indent = "\n";
|
||||
const tab = "\t";
|
||||
let i = 0;
|
||||
let pre: { indent: string; tag: string }[] = [];
|
||||
|
||||
html = html
|
||||
.replace(new RegExp("<pre>([\\s\\S]+?)?</pre>"), function (x) {
|
||||
pre.push({ indent: "", tag: x });
|
||||
return "<--TEMPPRE" + i++ + "/-->";
|
||||
})
|
||||
.replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) {
|
||||
let ret;
|
||||
const tagRegEx = /<\/?([^\s/>]+)/.exec(x);
|
||||
let tag = tagRegEx ? tagRegEx[1] : "";
|
||||
let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x);
|
||||
|
||||
if (p) {
|
||||
const pInd = parseInt(p[1]);
|
||||
pre[pInd].indent = indent;
|
||||
}
|
||||
|
||||
if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) {
|
||||
// self closing tag
|
||||
ret = indent + x;
|
||||
} else {
|
||||
if (x.indexOf("</") < 0) {
|
||||
//open tag
|
||||
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length);
|
||||
else ret = indent + x;
|
||||
!p && (indent += tab);
|
||||
} else {
|
||||
//close tag
|
||||
indent = indent.substr(0, indent.length - 1);
|
||||
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length);
|
||||
else ret = indent + x;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
for (i = pre.length; i--;) {
|
||||
html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("<pre>", "<pre>\n").replace("</pre>", pre[i].indent + "</pre>"));
|
||||
}
|
||||
|
||||
return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html;
|
||||
}
|
||||
|
||||
export async function clearBrowserCache() {
|
||||
if (isElectron()) {
|
||||
const win = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
@@ -491,7 +448,7 @@ function sleep(time_ms: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function escapeRegExp(str: string) {
|
||||
function escapeRegExp(str: string) {
|
||||
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
||||
}
|
||||
|
||||
@@ -873,23 +830,12 @@ export function getErrorMessage(e: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles left or right placement of e.g. tooltips in case of right-to-left languages. If the current language is a RTL one, then left and right are swapped. Other directions are unaffected.
|
||||
* @param placement a string optionally containing a "left" or "right" value.
|
||||
* @returns a left/right value swapped if needed, or the same as input otherwise.
|
||||
*/
|
||||
export function handleRightToLeftPlacement<T extends string>(placement: T) {
|
||||
if (!glob.isRtl) return placement;
|
||||
if (placement === "left") return "right";
|
||||
if (placement === "right") return "left";
|
||||
return placement;
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
restartDesktopApp,
|
||||
reloadTray,
|
||||
parseDate,
|
||||
getMonthsInDateRange,
|
||||
formatDateISO,
|
||||
formatDateTime,
|
||||
formatTimeInterval,
|
||||
@@ -897,7 +843,6 @@ export default {
|
||||
localNowDateTime,
|
||||
now,
|
||||
isElectron,
|
||||
isPWA,
|
||||
isMac,
|
||||
isCtrlKey,
|
||||
assertArguments,
|
||||
@@ -910,7 +855,6 @@ export default {
|
||||
getNoteTypeClass,
|
||||
getMimeTypeClass,
|
||||
isHtmlEmpty,
|
||||
formatHtml,
|
||||
clearBrowserCache,
|
||||
copySelectionToClipboard,
|
||||
dynamicRequire,
|
||||
|
||||
@@ -6,11 +6,9 @@ import frocaUpdater from "./froca_updater.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
import toast from "./toast.js";
|
||||
|
||||
type MessageHandler = (message: WebSocketMessage) => void;
|
||||
let messageHandlers: MessageHandler[] = [];
|
||||
type MessageHandler = (message: any) => void;
|
||||
const messageHandlers: MessageHandler[] = [];
|
||||
|
||||
let ws: WebSocket;
|
||||
let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
|
||||
@@ -49,14 +47,10 @@ function logInfo(message: string) {
|
||||
window.logError = logError;
|
||||
window.logInfo = logInfo;
|
||||
|
||||
export function subscribeToMessages(messageHandler: MessageHandler) {
|
||||
function subscribeToMessages(messageHandler: MessageHandler) {
|
||||
messageHandlers.push(messageHandler);
|
||||
}
|
||||
|
||||
export function unsubscribeToMessage(messageHandler: MessageHandler) {
|
||||
messageHandlers = messageHandlers.filter(handler => handler !== messageHandler);
|
||||
}
|
||||
|
||||
// used to serialize frontend update operations
|
||||
let consumeQueuePromise: Promise<void> | null = null;
|
||||
|
||||
@@ -279,17 +273,13 @@ function connectWebSocket() {
|
||||
|
||||
async function sendPing() {
|
||||
if (Date.now() - lastPingTs > 30000) {
|
||||
console.warn(utils.now(), "Lost websocket connection to the backend");
|
||||
toast.showPersistent({
|
||||
id: "lost-websocket-connection",
|
||||
title: t("ws.lost-websocket-connection-title"),
|
||||
message: t("ws.lost-websocket-connection-message"),
|
||||
icon: "no-signal"
|
||||
});
|
||||
console.log(
|
||||
utils.now(),
|
||||
"Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket."
|
||||
);
|
||||
}
|
||||
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
toast.closePersistent("lost-websocket-connection");
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "ping",
|
||||
@@ -304,8 +294,6 @@ async function sendPing() {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (glob.device === "print") return;
|
||||
|
||||
ws = connectWebSocket();
|
||||
|
||||
lastPingTs = Date.now();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
import "./stylesheets/auth.css";
|
||||
|
||||
// @TriliumNextTODO: is this even needed anymore?
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "jquery";
|
||||
import utils from "./services/utils.js";
|
||||
import ko from "knockout";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
|
||||
// TriliumNextTODO: properly make use of below types
|
||||
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
|
||||
|
||||
84
apps/client/src/share.ts
Normal file
84
apps/client/src/share.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import "normalize.css";
|
||||
import "boxicons/css/boxicons.min.css";
|
||||
import "@triliumnext/ckeditor5/content.css";
|
||||
import "@triliumnext/share-theme/styles/index.css";
|
||||
import "@triliumnext/share-theme/scripts/index.js";
|
||||
|
||||
async function ensureJQuery() {
|
||||
const $ = (await import("jquery")).default;
|
||||
(window as any).$ = $;
|
||||
}
|
||||
|
||||
async function applyMath() {
|
||||
const anyMathBlock = document.querySelector("#content .math-tex");
|
||||
if (!anyMathBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderMathInElement = (await import("./services/math.js")).renderMathInElement;
|
||||
renderMathInElement(document.getElementById("content"));
|
||||
}
|
||||
|
||||
async function formatCodeBlocks() {
|
||||
const anyCodeBlock = document.querySelector("#content pre");
|
||||
if (!anyCodeBlock) {
|
||||
return;
|
||||
}
|
||||
await ensureJQuery();
|
||||
const { formatCodeBlocks } = await import("./services/syntax_highlight.js");
|
||||
await formatCodeBlocks($("#content"));
|
||||
}
|
||||
|
||||
async function setupTextNote() {
|
||||
formatCodeBlocks();
|
||||
applyMath();
|
||||
|
||||
const setupMermaid = (await import("./share/mermaid.js")).default;
|
||||
setupMermaid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch note with given ID from backend
|
||||
*
|
||||
* @param noteId of the given note to be fetched. If false, fetches current note.
|
||||
*/
|
||||
async function fetchNote(noteId: string | null = null) {
|
||||
if (!noteId) {
|
||||
noteId = document.body.getAttribute("data-note-id");
|
||||
}
|
||||
|
||||
const resp = await fetch(`api/notes/${noteId}`);
|
||||
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => {
|
||||
const noteType = determineNoteType();
|
||||
|
||||
if (noteType === "text") {
|
||||
setupTextNote();
|
||||
}
|
||||
|
||||
const toggleMenuButton = document.getElementById("toggleMenuButton");
|
||||
const layout = document.getElementById("layout");
|
||||
|
||||
if (toggleMenuButton && layout) {
|
||||
toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu"));
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
function determineNoteType() {
|
||||
const bodyClass = document.body.className;
|
||||
const match = bodyClass.match(/type-([^\s]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// workaround to prevent webpack from removing "fetchNote" as dead code:
|
||||
// add fetchNote as property to the window object
|
||||
Object.defineProperty(window, "fetchNote", {
|
||||
value: fetchNote
|
||||
});
|
||||
@@ -1,12 +1,7 @@
|
||||
export default async function setupMermaid() {
|
||||
const mermaidEls = document.querySelectorAll("#content pre code.language-mermaid");
|
||||
if (mermaidEls.length === 0) {
|
||||
return;
|
||||
}
|
||||
import mermaid from "mermaid";
|
||||
|
||||
const mermaid = (await import("mermaid")).default;
|
||||
|
||||
for (const codeBlock of mermaidEls) {
|
||||
export default function setupMermaid() {
|
||||
for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) {
|
||||
const parentPre = codeBlock.parentElement;
|
||||
if (!parentPre) {
|
||||
continue;
|
||||
2
apps/client/src/stylesheets/bootstrap.scss
vendored
Normal file
2
apps/client/src/stylesheets/bootstrap.scss
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Import all of Bootstrap's CSS */
|
||||
@use "bootstrap/scss/bootstrap";
|
||||
@@ -60,7 +60,7 @@
|
||||
appearance: none;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
border-inline-start: unset;
|
||||
border-left: unset;
|
||||
background-color: var(--menu-background-color);
|
||||
font-weight: bold;
|
||||
outline: 0;
|
||||
@@ -102,7 +102,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: var(--main-border-color);
|
||||
|
||||
@@ -299,7 +299,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: -100%;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, var(--hover-item-background-color, rgba(0, 0, 0, 0.03)), transparent);
|
||||
@@ -406,10 +406,10 @@
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
inset-inline-start: -100%;
|
||||
left: -100%;
|
||||
}
|
||||
100% {
|
||||
inset-inline-start: 100%;
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
322
apps/client/src/stylesheets/print.css
Normal file
322
apps/client/src/stylesheets/print.css
Normal file
@@ -0,0 +1,322 @@
|
||||
:root {
|
||||
--main-background-color: white;
|
||||
--root-background: var(--main-background-color);
|
||||
--launcher-pane-background-color: var(--main-background-color);
|
||||
--main-text-color: black;
|
||||
--input-text-color: var(--main-text-color);
|
||||
|
||||
--print-font-size: 11pt;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 2cm;
|
||||
}
|
||||
|
||||
.ck-content {
|
||||
font-size: var(--print-font-size);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.note-detail-readonly-text {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.no-print,
|
||||
.no-print *,
|
||||
.tab-row-container,
|
||||
.tab-row-widget,
|
||||
.title-bar-buttons,
|
||||
#launcher-pane,
|
||||
#left-pane,
|
||||
#center-pane > *:not(.split-note-container-widget),
|
||||
#right-pane,
|
||||
.title-row .note-icon-widget,
|
||||
.title-row .button-widget,
|
||||
.ribbon-container,
|
||||
.promoted-attributes-widget,
|
||||
.scroll-padding-widget,
|
||||
.note-list-widget,
|
||||
.spacer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.mobile #mobile-sidebar-wrapper,
|
||||
body.mobile .classic-toolbar-widget,
|
||||
body.mobile .action-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.mobile #detail-container {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
body.mobile .note-title-widget {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
body,
|
||||
#root-widget,
|
||||
#rest-pane > div.component:first-child,
|
||||
.note-detail-printable,
|
||||
.note-detail-editable-text-editor {
|
||||
height: unset !important;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ck.ck-editor__editable_inline {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.note-title-widget input,
|
||||
.note-detail-editable-text,
|
||||
.note-detail-editable-text-editor {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: unset !important;
|
||||
height: unset !important;
|
||||
overflow: visible;
|
||||
position: unset;
|
||||
/* https://github.com/zadam/trilium/issues/3202 */
|
||||
color: black;
|
||||
}
|
||||
|
||||
#root-widget,
|
||||
#horizontal-main-container,
|
||||
#rest-pane,
|
||||
#vertical-main-container,
|
||||
#center-pane,
|
||||
.split-note-container-widget,
|
||||
.note-split:not(.hidden-ext),
|
||||
body.mobile #mobile-rest-container {
|
||||
display: block !important;
|
||||
overflow: auto;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
#center-pane,
|
||||
#rest-pane,
|
||||
.note-split,
|
||||
body.mobile #detail-container {
|
||||
width: unset !important;
|
||||
max-width: unset !important;
|
||||
}
|
||||
|
||||
.component {
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
/* Respect page breaks */
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
break-after: always;
|
||||
}
|
||||
|
||||
.page-break > * {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.relation-map-wrapper {
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
.table thead th,
|
||||
.table td,
|
||||
.table th {
|
||||
/* Fix center vertical alignment of table cells */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
pre {
|
||||
box-shadow: unset !important;
|
||||
border: 0.75pt solid gray !important;
|
||||
border-radius: 2pt !important;
|
||||
}
|
||||
|
||||
th,
|
||||
span[style] {
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
/*
|
||||
* Text note specific fixes
|
||||
*/
|
||||
.ck-widget {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.ck-placeholder,
|
||||
.ck-widget__type-around,
|
||||
.ck-widget__selection-handle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ck-widget.table td.ck-editor__nested-editable.ck-editor__nested-editable_focused,
|
||||
.ck-widget.table td.ck-editor__nested-editable:focus,
|
||||
.ck-widget.table th.ck-editor__nested-editable.ck-editor__nested-editable_focused,
|
||||
.ck-widget.table th.ck-editor__nested-editable:focus {
|
||||
background: unset !important;
|
||||
outline: unset !important;
|
||||
}
|
||||
|
||||
.include-note .include-note-content {
|
||||
max-height: unset !important;
|
||||
overflow: unset !important;
|
||||
}
|
||||
|
||||
/* TODO: This will break once we translate the language */
|
||||
.ck-content pre[data-language="Auto-detected"]:after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Code note specific fixes.
|
||||
*/
|
||||
.note-detail-code pre {
|
||||
border: unset !important;
|
||||
border-radius: unset !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Links
|
||||
*/
|
||||
|
||||
.note-detail-printable a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.note-detail-printable a:not([href^="#root/"]) {
|
||||
text-decoration: underline;
|
||||
color: #374a75;
|
||||
}
|
||||
|
||||
.note-detail-printable a::after {
|
||||
/* Hide the external link trailing arrow */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO list check boxes
|
||||
*/
|
||||
|
||||
.note-detail-printable .todo-list__label * {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
|
||||
.note-detail-printable .todo-list__label__description {
|
||||
/* The percentage of the line height that the check box occupies */
|
||||
--box-ratio: 0.75;
|
||||
/* The size of the gap between the check box and the caption */
|
||||
--box-text-gap: 0.25em;
|
||||
|
||||
--box-size: calc(1lh * var(--box-ratio));
|
||||
--box-vert-offset: calc((1lh - var(--box-size)) / 2);
|
||||
|
||||
display: inline-block;
|
||||
padding-left: calc(var(--box-size) + var(--box-text-gap));
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-blank-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5C3.89%2c3 3%2c3.89 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5C21%2c3.89 20.1%2c3 19%2c3M19%2c5V19H5V5H19Z' /%3e%3c/svg%3e");
|
||||
background-position: 0 var(--box-vert-offset);
|
||||
background-size: var(--box-size);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.note-detail-printable .todo-list__label:has(input[type="checkbox"]:checked) .todo-list__label__description {
|
||||
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-outline/ */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5A2%2c2 0 0%2c0 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5A2%2c2 0 0%2c0 19%2c3M19%2c5V19H5V5H19M10%2c17L6%2c13L7.41%2c11.58L10%2c14.17L16.59%2c7.58L18%2c9' /%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.note-detail-printable .todo-list__label input[type="checkbox"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Blockquotes
|
||||
*/
|
||||
|
||||
.note-detail-printable blockquote {
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
/*
|
||||
* Figures
|
||||
*/
|
||||
|
||||
.note-detail-printable figcaption {
|
||||
--accented-background-color: transparent;
|
||||
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/*
|
||||
* Footnotes
|
||||
*/
|
||||
|
||||
.note-detail-printable .footnote-reference a,
|
||||
.footnote-back-link a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Make the "^" link cover the whole area of the footnote item */
|
||||
|
||||
.footnote-section {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.note-detail-printable li.footnote-item {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.note-detail-printable .footnote-back-link,
|
||||
.note-detail-printable .footnote-back-link *,
|
||||
.note-detail-printable .footnote-back-link a {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-printable .footnote-back-link a {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.note-detail-printable .footnote-content {
|
||||
display: inline-block;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
/*
|
||||
* Widows and orphans
|
||||
*/
|
||||
p,
|
||||
blockquote {
|
||||
widows: 4;
|
||||
orphans: 4;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
widows: 6;
|
||||
orphans: 6;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
.note-detail-relation-map .endpoint {
|
||||
position: absolute;
|
||||
bottom: 37%;
|
||||
inset-inline-end: 5px;
|
||||
right: 5px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-color: #333;
|
||||
|
||||
@@ -161,8 +161,7 @@ textarea,
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.form-group.disabled,
|
||||
.form-checkbox.disabled {
|
||||
.form-group.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -174,12 +173,12 @@ textarea,
|
||||
/* Add a gap between consecutive radios / check boxes */
|
||||
label.tn-radio + label.tn-radio,
|
||||
label.tn-checkbox + label.tn-checkbox {
|
||||
margin-inline-start: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
label.tn-radio input[type="radio"],
|
||||
label.tn-checkbox input[type="checkbox"] {
|
||||
margin-inline-end: .5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
#left-pane input,
|
||||
@@ -226,7 +225,7 @@ samp {
|
||||
.badge {
|
||||
--bs-badge-color: var(--muted-text-color);
|
||||
|
||||
margin-inline-start: 8px;
|
||||
margin-left: 8px;
|
||||
background: var(--accented-background-color);
|
||||
}
|
||||
|
||||
@@ -252,6 +251,10 @@ button.close:hover {
|
||||
color: var(--main-text-color) !important;
|
||||
}
|
||||
|
||||
.note-title[readonly] {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.tdialog {
|
||||
display: none;
|
||||
}
|
||||
@@ -290,11 +293,6 @@ button.close:hover {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.icon-action.btn {
|
||||
padding: 0 8px;
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
.ui-widget-content a:not(.ui-tabs-anchor) {
|
||||
color: #337ab7 !important;
|
||||
}
|
||||
@@ -338,8 +336,8 @@ button kbd {
|
||||
}
|
||||
|
||||
.ui-menu kbd {
|
||||
margin-inline-start: 30px;
|
||||
float: inline-end;
|
||||
margin-left: 30px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.suppressed {
|
||||
@@ -360,23 +358,24 @@ button kbd {
|
||||
}
|
||||
|
||||
.dropdown-menu,
|
||||
.tabulator-popup-container,
|
||||
:root .excalidraw .popover {
|
||||
.tabulator-popup-container {
|
||||
color: var(--menu-text-color) !important;
|
||||
font-size: inherit;
|
||||
background: var(--menu-background-color) !important;
|
||||
background-color: var(--menu-background-color) !important;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
--bs-dropdown-zindex: 999;
|
||||
--bs-dropdown-link-active-bg: var(--active-item-background-color) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-divider {
|
||||
break-before: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu,
|
||||
body.desktop .tabulator-popup-container,
|
||||
:root .excalidraw .dropdown-menu .dropdown-menu-container,
|
||||
:root .excalidraw .popover {
|
||||
body.desktop .tabulator-popup-container {
|
||||
border: 1px solid var(--dropdown-border-color);
|
||||
column-rule: 1px solid var(--dropdown-border-color);
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
animation: dropdown-menu-opening 100ms ease-in;
|
||||
}
|
||||
@@ -395,7 +394,7 @@ body.desktop .tabulator-popup-container,
|
||||
}
|
||||
|
||||
.dropend .dropdown-toggle::after {
|
||||
margin-inline-start: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
@@ -406,7 +405,7 @@ body.desktop .tabulator-popup-container,
|
||||
|
||||
.dropdown-menu .disabled .disabled-tooltip {
|
||||
pointer-events: all;
|
||||
margin-inline-start: 8px;
|
||||
margin-left: 8px;
|
||||
font-size: 0.5em;
|
||||
color: var(--disabled-tooltip-icon-color);
|
||||
cursor: help;
|
||||
@@ -418,18 +417,17 @@ body.desktop .tabulator-popup-container,
|
||||
}
|
||||
|
||||
.dropdown-menu a:hover:not(.disabled),
|
||||
.dropdown-item:hover:not(.disabled, .dropdown-container-item),
|
||||
.tabulator-menu-item:hover,
|
||||
:root .excalidraw .context-menu .context-menu-item:hover {
|
||||
.dropdown-item:hover:not(.disabled, .dropdown-item-container),
|
||||
.tabulator-menu-item:hover {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--hover-item-border-color) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-container-item,
|
||||
.dropdown-item.dropdown-container-item:hover,
|
||||
.dropdown-container-item:active {
|
||||
.dropdown-item-container,
|
||||
.dropdown-item-container:hover,
|
||||
.dropdown-item-container:active {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -444,11 +442,9 @@ body #context-menu-container .dropdown-item > span {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-item span.keyboard-shortcut,
|
||||
.dropdown-item *:not(.keyboard-shortcut) > kbd {
|
||||
.dropdown-item span.keyboard-shortcut {
|
||||
flex-grow: 1;
|
||||
text-align: end;
|
||||
padding-inline-start: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdown-menu kbd {
|
||||
@@ -458,21 +454,16 @@ body #context-menu-container .dropdown-item > span {
|
||||
box-shadow: none;
|
||||
padding-bottom: 0;
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdown-item,
|
||||
.dropdown-header,
|
||||
:root .excalidraw .context-menu .context-menu-item:hover {
|
||||
.dropdown-header {
|
||||
color: var(--menu-text-color) !important;
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
|
||||
* It usually wraps a menu item followed by a separator / header and another menu item. */
|
||||
.dropdown-no-break {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled,
|
||||
.dropdown-item.disabled kbd {
|
||||
color: #aaa !important;
|
||||
@@ -480,9 +471,9 @@ body #context-menu-container .dropdown-item > span {
|
||||
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:focus {
|
||||
color: var(--active-item-text-color);
|
||||
background-color: var(--active-item-background-color);
|
||||
border-color: var(--active-item-border-color);
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--active-item-border-color) !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -509,7 +500,7 @@ body #context-menu-container .dropdown-item > span {
|
||||
|
||||
body .cm-editor .cm-gutters {
|
||||
background-color: inherit !important;
|
||||
border-inline-end: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
body .cm-editor .cm-placeholder {
|
||||
@@ -591,10 +582,6 @@ button.btn-sm {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
body[dir=rtl] .ck.ck-block-toolbar-button {
|
||||
transform: translateX(-7px);
|
||||
}
|
||||
|
||||
pre:not(.hljs) {
|
||||
color: var(--main-text-color) !important;
|
||||
white-space: pre-wrap;
|
||||
@@ -613,11 +600,11 @@ pre:not(.hljs) {
|
||||
pre > button.copy-button {
|
||||
position: absolute;
|
||||
top: var(--copy-button-margin-size);
|
||||
inset-inline-end: var(--copy-button-margin-size);
|
||||
right: var(--copy-button-margin-size);
|
||||
}
|
||||
|
||||
:root pre:has(> button.copy-button) {
|
||||
padding-inline-end: calc(var(--copy-button-width) + (var(--copy-button-margin-size) * 2));
|
||||
padding-right: calc(var(--copy-button-width) + (var(--copy-button-margin-size) * 2));
|
||||
}
|
||||
|
||||
pre > button.copy-button:hover {
|
||||
@@ -643,31 +630,31 @@ pre > button.copy-button:active {
|
||||
.full-text-search-button {
|
||||
cursor: pointer;
|
||||
font-size: 1.3em;
|
||||
padding-inline-start: 5px;
|
||||
padding-inline-end: 5px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.input-clearer-button {
|
||||
cursor: pointer;
|
||||
font-size: 1.3em;
|
||||
background: inherit !important;
|
||||
padding-inline-start: 5px;
|
||||
padding-inline-end: 5px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.open-external-link-button {
|
||||
cursor: pointer;
|
||||
font-size: 1.3em;
|
||||
padding-inline-start: 5px;
|
||||
padding-inline-end: 5px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.go-to-selected-note-button {
|
||||
cursor: pointer;
|
||||
font-size: 1.3em;
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: 3px;
|
||||
padding-left: 4px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.go-to-selected-note-button.disabled,
|
||||
@@ -680,7 +667,7 @@ pre > button.copy-button:active {
|
||||
|
||||
.note-autocomplete-input {
|
||||
/* this is for seamless integration of "input clearer" button */
|
||||
border-inline-end: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
table.promoted-attributes-in-tooltip {
|
||||
@@ -713,10 +700,10 @@ table.promoted-attributes-in-tooltip th {
|
||||
border-top-color: var(--main-border-color) !important;
|
||||
}
|
||||
.bs-tooltip-left .tooltip-arrow::before {
|
||||
border-inline-start-color: var(--main-border-color) !important;
|
||||
border-left-color: var(--main-border-color) !important;
|
||||
}
|
||||
.bs-tooltip-right .tooltip-arrow::before {
|
||||
border-inline-end-color: var(--main-border-color) !important;
|
||||
border-right-color: var(--main-border-color) !important;
|
||||
}
|
||||
|
||||
.bs-tooltip-bottom .tooltip-arrow::after {
|
||||
@@ -726,17 +713,17 @@ table.promoted-attributes-in-tooltip th {
|
||||
border-top-color: var(--tooltip-background-color) !important;
|
||||
}
|
||||
.bs-tooltip-left .tooltip-arrow::after {
|
||||
border-inline-start-color: var(--tooltip-background-color) !important;
|
||||
border-left-color: var(--tooltip-background-color) !important;
|
||||
}
|
||||
.bs-tooltip-right .tooltip-arrow::after {
|
||||
border-inline-end-color: var(--tooltip-background-color) !important;
|
||||
border-right-color: var(--tooltip-background-color) !important;
|
||||
}
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="left"] .tooltip-arrow::before,
|
||||
.bs-tooltip-left .tooltip-arrow::before {
|
||||
inset-inline-start: -1px;
|
||||
left: -1px;
|
||||
border-width: 0.4rem 0 0.4rem 0.4rem;
|
||||
border-inline-start-color: var(--main-border-color) !important;
|
||||
border-left-color: var(--main-border-color) !important;
|
||||
}
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="bottom"] .tooltip-arrow::before,
|
||||
@@ -748,9 +735,9 @@ table.promoted-attributes-in-tooltip th {
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="right"] .tooltip-arrow::before,
|
||||
.bs-tooltip-right .tooltip-arrow::before {
|
||||
inset-inline-end: -1px;
|
||||
right: -1px;
|
||||
border-width: 0.4rem 0.4rem 0.4rem 0;
|
||||
border-inline-end-color: var(--main-border-color) !important;
|
||||
border-right-color: var(--main-border-color) !important;
|
||||
}
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="top"] .tooltip-arrow::before,
|
||||
@@ -762,9 +749,9 @@ table.promoted-attributes-in-tooltip th {
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="left"] .tooltip-arrow::after,
|
||||
.bs-tooltip-left .tooltip-arrow::after {
|
||||
inset-inline-start: -1px;
|
||||
left: -1px;
|
||||
border-width: 0.4rem 0 0.4rem 0.4rem;
|
||||
border-inline-start-color: var(--tooltip-background-color) !important;
|
||||
border-left-color: var(--tooltip-background-color) !important;
|
||||
}
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="bottom"] .tooltip-arrow::after,
|
||||
@@ -776,9 +763,9 @@ table.promoted-attributes-in-tooltip th {
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="right"] .tooltip-arrow::after,
|
||||
.bs-tooltip-right .tooltip-arrow::after {
|
||||
inset-inline-end: -1px;
|
||||
right: -1px;
|
||||
border-width: 0.4rem 0.4rem 0.4rem 0;
|
||||
border-inline-end-color: var(--tooltip-background-color) !important;
|
||||
border-right-color: var(--tooltip-background-color) !important;
|
||||
}
|
||||
|
||||
.bs-tooltip-auto[data-popper-placement^="top"] .tooltip-arrow::after,
|
||||
@@ -797,7 +784,7 @@ table.promoted-attributes-in-tooltip th {
|
||||
background-color: var(--tooltip-background-color) !important;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 5px;
|
||||
text-align: start;
|
||||
text-align: left;
|
||||
color: var(--main-text-color) !important;
|
||||
max-width: 500px;
|
||||
box-shadow: 10px 10px 93px -25px #aaaaaa;
|
||||
@@ -830,7 +817,7 @@ table.promoted-attributes-in-tooltip th {
|
||||
|
||||
.note-tooltip-content .open-popup-button {
|
||||
position: absolute;
|
||||
inset-inline-end: 15px;
|
||||
right: 15px;
|
||||
bottom: 8px;
|
||||
font-size: 1.2em;
|
||||
color: inherit;
|
||||
@@ -850,7 +837,7 @@ table.promoted-attributes-in-tooltip th {
|
||||
}
|
||||
|
||||
.tooltip-inner figure.image-style-side {
|
||||
float: inline-end;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.tooltip.show {
|
||||
@@ -899,7 +886,7 @@ table.promoted-attributes-in-tooltip th {
|
||||
.aa-dropdown-menu .aa-suggestion .text {
|
||||
display: inline-block;
|
||||
width: calc(100% - 20px);
|
||||
padding-inline-start: 4px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.aa-dropdown-menu .aa-suggestion .search-result-title {
|
||||
@@ -925,7 +912,7 @@ table.promoted-attributes-in-tooltip th {
|
||||
}
|
||||
|
||||
.help-button {
|
||||
float: inline-end;
|
||||
float: right;
|
||||
background: none;
|
||||
font-weight: 900;
|
||||
color: orange;
|
||||
@@ -1013,7 +1000,7 @@ svg.ck-icon .note-icon {
|
||||
--ck-content-line-height: var(--bs-body-line-height);
|
||||
}
|
||||
|
||||
:root .ck-content .table table:not(.layout-table) th {
|
||||
.ck-content .table table th {
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
|
||||
@@ -1042,7 +1029,7 @@ svg.ck-icon .note-icon {
|
||||
counter-increment: footnote-counter;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin-inline-start: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.ck-content .footnote-item > * {
|
||||
@@ -1050,13 +1037,13 @@ svg.ck-icon .note-icon {
|
||||
}
|
||||
|
||||
.ck-content .footnote-back-link {
|
||||
margin-inline-end: 0.1em;
|
||||
margin-right: 0.1em;
|
||||
position: relative;
|
||||
top: -0.2em;
|
||||
}
|
||||
|
||||
.ck-content .footnotes .footnote-back-link > sup {
|
||||
margin-inline-end: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ck-content .footnote-item:before {
|
||||
@@ -1064,8 +1051,8 @@ svg.ck-icon .note-icon {
|
||||
display: inline-block;
|
||||
min-width: fit-content;
|
||||
position: relative;
|
||||
inset-inline-end: 0.2em;
|
||||
text-align: end;
|
||||
right: 0.2em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ck-content .footnote-content {
|
||||
@@ -1081,11 +1068,11 @@ svg.ck-icon .note-icon {
|
||||
}
|
||||
|
||||
#options-dialog input[type="number"] {
|
||||
text-align: end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.help-cards ul {
|
||||
padding-inline-start: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-cards kbd {
|
||||
@@ -1147,27 +1134,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
|
||||
.toast-body {
|
||||
white-space: preserve-breaks;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toast.no-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.toast.no-title .toast-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
|
||||
}
|
||||
|
||||
.toast.no-title .toast-body {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.toast.no-title .toast-header {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.ck-mentions .ck-button {
|
||||
@@ -1276,15 +1242,11 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.hidden-ext.note-split + .gutter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#context-menu-cover.show {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
@@ -1297,8 +1259,8 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
|
||||
body.mobile #context-menu-container.mobile-bottom-menu {
|
||||
position: fixed !important;
|
||||
inset-inline-start: 0 !important;
|
||||
inset-inline-end: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
top: unset !important;
|
||||
max-height: 70vh;
|
||||
@@ -1348,7 +1310,7 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
|
||||
|
||||
.dropdown-submenu > .dropdown-menu {
|
||||
top: 0;
|
||||
inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
|
||||
left: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
|
||||
margin-top: -10px;
|
||||
min-width: 15rem;
|
||||
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
|
||||
@@ -1357,7 +1319,7 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
|
||||
}
|
||||
|
||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: calc(-100% + 10px);
|
||||
left: calc(-100% + 10px);
|
||||
}
|
||||
|
||||
.right-dropdown-widget {
|
||||
@@ -1423,7 +1385,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
|
||||
.ck.ck-slash-command-button__text-part,
|
||||
.ck.ck-template-form__text-part {
|
||||
margin-inline-start: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
line-height: 1.2em !important;
|
||||
}
|
||||
|
||||
@@ -1454,8 +1416,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
}
|
||||
|
||||
.area-expander-text {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1501,7 +1463,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
color: var(--launcher-pane-text-color);
|
||||
background-color: transparent;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1542,16 +1504,16 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
position: fixed !important;
|
||||
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)) !important;
|
||||
top: unset !important;
|
||||
inset-inline-start: 0 !important;
|
||||
inset-inline-end: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
transform: unset !important;
|
||||
}
|
||||
|
||||
#mobile-sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
transition: background-color 250ms ease-in-out;
|
||||
@@ -1566,7 +1528,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
#mobile-sidebar-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 85vw;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
@@ -1598,8 +1560,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
body.mobile .modal-dialog {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 !important;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
@@ -1757,8 +1719,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: 5px;
|
||||
margin-left: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#right-pane .card-header {
|
||||
@@ -1798,7 +1760,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
}
|
||||
|
||||
#right-pane .card-body ul {
|
||||
padding-inline-start: 25px;
|
||||
padding-left: 25px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@@ -1809,8 +1771,9 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
}
|
||||
|
||||
.note-split {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
flex-basis: 0; /* so that each split has same width */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.note-split.full-content-width {
|
||||
@@ -1829,7 +1792,7 @@ button.close:hover {
|
||||
.reference-link .bx {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-inline-end: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.options-section:first-of-type h4 {
|
||||
@@ -1867,7 +1830,7 @@ textarea {
|
||||
|
||||
.attachment-help-button {
|
||||
display: inline-block;
|
||||
margin-inline-start: 10px;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -1905,6 +1868,11 @@ textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-suggestion:hover,
|
||||
.jump-to-note-results .aa-dropdown-menu .aa-cursor {
|
||||
background-color: var(--hover-item-background-color, #f8f9fa);
|
||||
}
|
||||
|
||||
/* Command palette styling */
|
||||
.jump-to-note-dialog .command-suggestion {
|
||||
display: flex;
|
||||
@@ -1975,7 +1943,7 @@ textarea {
|
||||
}
|
||||
|
||||
body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
|
||||
padding-inline-start: 1em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
#tab-row-left-spacer {
|
||||
@@ -1983,12 +1951,8 @@ body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
body.electron.platform-darwin:not(.native-titlebar) #tab-row-left-spacer {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.tab-row-widget {
|
||||
padding-inline-end: calc(100vw - env(titlebar-area-width, 100vw));
|
||||
padding-right: calc(100vw - env(titlebar-area-width, 100vw));
|
||||
}
|
||||
|
||||
.tab-row-container .toggle-button {
|
||||
@@ -2034,24 +1998,20 @@ body.zen #right-pane,
|
||||
body.zen #mobile-sidebar-wrapper,
|
||||
body.zen .tab-row-container,
|
||||
body.zen .tab-row-widget,
|
||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
|
||||
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
|
||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
|
||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget.visible)),
|
||||
body.zen .ribbon-container:has(.classic-toolbar-widget.visible) .ribbon-top-row,
|
||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget.visible)),
|
||||
body.zen .note-icon-widget,
|
||||
body.zen .title-row .icon-action,
|
||||
body.zen .title-row .button-widget,
|
||||
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
|
||||
body.zen .action-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.zen .split-note-container-widget > .gutter {
|
||||
display: unset !important;
|
||||
}
|
||||
|
||||
body.zen #launcher-pane {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
inset-inline-end: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 64px !important;
|
||||
height: 64px !important;
|
||||
background: transparent !important;
|
||||
@@ -2062,8 +2022,8 @@ body.zen .title-row {
|
||||
display: block !important;
|
||||
height: unset !important;
|
||||
-webkit-app-region: drag;
|
||||
padding-inline-start: env(titlebar-area-x);
|
||||
padding-inline-end: calc(100vw - env(titlebar-area-width, 100vw) + 2.5em);
|
||||
padding-left: env(titlebar-area-x);
|
||||
padding-right: calc(100vw - env(titlebar-area-width, 100vw) + 2.5em);
|
||||
}
|
||||
|
||||
body.zen .floating-buttons {
|
||||
@@ -2071,7 +2031,7 @@ body.zen .floating-buttons {
|
||||
}
|
||||
|
||||
body.zen .floating-buttons-children {
|
||||
inset-inline-end: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
body.zen .floating-buttons-children .button-widget {
|
||||
@@ -2230,7 +2190,7 @@ footer.webview-footer button {
|
||||
.chat-input {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
padding-inline-end: 40px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.chat-buttons {
|
||||
@@ -2286,13 +2246,14 @@ footer.webview-footer button {
|
||||
|
||||
.admonition {
|
||||
--accent-color: var(--card-border-color);
|
||||
background: color-mix(in srgb, var(--accent-color) 15%, transparent);
|
||||
border: 1px solid var(--accent-color);
|
||||
box-shadow: var(--card-box-shadow);
|
||||
background: var(--card-background-color);
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
margin: 1.25em 0;
|
||||
position: relative;
|
||||
padding-inline-start: 2.5em;
|
||||
padding-left: 2.5em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2305,7 +2266,7 @@ footer.webview-footer button {
|
||||
font-family: boxicons !important;
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
inset-inline-start: 1em;
|
||||
left: 1em;
|
||||
}
|
||||
|
||||
.admonition.note { --accent-color: var(--admonition-note-accent-color); }
|
||||
@@ -2331,18 +2292,18 @@ footer.webview-footer button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
margin-inline-end: 15px;
|
||||
margin-right: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-option input[type="checkbox"] {
|
||||
margin-inline-end: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Style for thinking process in chat responses */
|
||||
.thinking-process {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-inline-start: 3px solid var(--main-text-color);
|
||||
border-left: 3px solid var(--main-text-color);
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
@@ -2350,23 +2311,23 @@ footer.webview-footer button {
|
||||
|
||||
.thinking-step {
|
||||
margin-bottom: 8px;
|
||||
padding-inline-start: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.thinking-step.observation {
|
||||
border-inline-start: 2px solid #69c7ff;
|
||||
border-left: 2px solid #69c7ff;
|
||||
}
|
||||
|
||||
.thinking-step.hypothesis {
|
||||
border-inline-start: 2px solid #9839f7;
|
||||
border-left: 2px solid #9839f7;
|
||||
}
|
||||
|
||||
.thinking-step.evidence {
|
||||
border-inline-start: 2px solid #40c025;
|
||||
border-left: 2px solid #40c025;
|
||||
}
|
||||
|
||||
.thinking-step.conclusion {
|
||||
border-inline-start: 2px solid #e2aa03;
|
||||
border-left: 2px solid #e2aa03;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -2381,17 +2342,17 @@ footer.webview-footer button {
|
||||
|
||||
.content-floating-buttons.top-left {
|
||||
top: 10px;
|
||||
inset-inline-start: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.content-floating-buttons.bottom-left {
|
||||
bottom: 10px;
|
||||
inset-inline-start: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.content-floating-buttons.bottom-right {
|
||||
bottom: 10px;
|
||||
inset-inline-end: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.content-floating-buttons button.bx {
|
||||
@@ -2413,27 +2374,4 @@ footer.webview-footer button {
|
||||
max-width: 25vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.revision-diff-added {
|
||||
background: rgba(100, 200, 100, 0.5);
|
||||
}
|
||||
|
||||
.revision-diff-removed {
|
||||
background: rgba(255, 100, 100, 0.5);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
iframe.print-iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -600px;
|
||||
right: -600px;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.excalidraw.theme--dark canvas {
|
||||
--theme-filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user