mirror of
https://github.com/zadam/trilium.git
synced 2025-12-16 05:09:54 +01:00
Compare commits
1 Commits
v0.100.0
...
weblate-tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d2984f1d |
4
.github/actions/build-server/action.yml
vendored
4
.github/actions/build-server/action.yml
vendored
@@ -10,9 +10,9 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
2
.github/actions/report-size/action.yml
vendored
2
.github/actions/report-size/action.yml
vendored
@@ -44,7 +44,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
# Checkout branch to compare to [required]
|
# Checkout branch to compare to [required]
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.branch }}
|
ref: ${{ inputs.branch }}
|
||||||
path: br-base
|
path: br-base
|
||||||
|
|||||||
334
.github/copilot-instructions.md
vendored
334
.github/copilot-instructions.md
vendored
@@ -1,334 +0,0 @@
|
|||||||
# Trilium Notes - AI Coding Agent Instructions
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. Built as a TypeScript monorepo using pnpm, it implements a three-layer caching architecture (Becca/Froca/Shaca) with a widget-based UI system and supports extensive user scripting capabilities.
|
|
||||||
|
|
||||||
## Essential Architecture Patterns
|
|
||||||
|
|
||||||
### Three-Layer Cache System (Critical to Understand)
|
|
||||||
- **Becca** (`apps/server/src/becca/`): Server-side entity cache, primary data source
|
|
||||||
- **Froca** (`apps/client/src/services/froca.ts`): Client-side mirror synchronized via WebSocket
|
|
||||||
- **Shaca** (`apps/server/src/share/`): Optimized cache for public/shared notes
|
|
||||||
|
|
||||||
**Key insight**: Never bypass these caches with direct DB queries. Always use `becca.notes[noteId]`, `froca.getNote()`, or equivalent cache methods.
|
|
||||||
|
|
||||||
### Entity Relationship Model
|
|
||||||
Notes use a **multi-parent tree** via branches:
|
|
||||||
- `BNote` - The note content and metadata
|
|
||||||
- `BBranch` - Tree relationships (one note can have multiple parents via cloning)
|
|
||||||
- `BAttribute` - Key-value metadata attached to notes (labels and relations)
|
|
||||||
|
|
||||||
### Entity Change System & Sync
|
|
||||||
Every entity modification (notes, branches, attributes) creates an `EntityChange` record that drives synchronization:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Entity changes are automatically tracked
|
|
||||||
note.title = "New Title";
|
|
||||||
note.save(); // Creates EntityChange record with changeId
|
|
||||||
|
|
||||||
// Sync protocol via WebSocket
|
|
||||||
ws.sendMessage({ type: 'sync-pull-in-progress', ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
**Critical**: This is why you must use Becca/Froca methods instead of direct DB writes - they create the change tracking records needed for sync.
|
|
||||||
|
|
||||||
### Entity Lifecycle & Events
|
|
||||||
The event system (`apps/server/src/services/events.ts`) broadcasts entity lifecycle events:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Subscribe to events in widgets or services
|
|
||||||
eventService.subscribe('noteChanged', ({ noteId }) => {
|
|
||||||
// React to note changes
|
|
||||||
});
|
|
||||||
|
|
||||||
// Common events: noteChanged, branchChanged, attributeChanged, noteDeleted
|
|
||||||
// Widget method: entitiesReloadedEvent({loadResults}) for handling reloads
|
|
||||||
```
|
|
||||||
|
|
||||||
**Becca loader priorities**: Events are emitted in order (notes → branches → attributes) during initial load to ensure referential integrity.
|
|
||||||
|
|
||||||
### TaskContext for Long Operations
|
|
||||||
Use `TaskContext` for operations with progress reporting (imports, exports, bulk operations):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const taskContext = new TaskContext("task-id", "import", "Import Notes");
|
|
||||||
taskContext.increaseProgressCount();
|
|
||||||
|
|
||||||
// WebSocket messages: { type: 'taskProgressCount', taskId, taskType, data, progressCount }
|
|
||||||
|
|
||||||
**Pattern**: All long-running operations (delete note trees, export, import) use TaskContext to send WebSocket updates to the frontend.
|
|
||||||
|
|
||||||
### Protected Session Handling
|
|
||||||
Protected notes require an active encryption session:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Always check before accessing protected content
|
|
||||||
if (note.isContentAvailable()) {
|
|
||||||
const content = note.getContent(); // Safe
|
|
||||||
} else {
|
|
||||||
const title = note.getTitleOrProtected(); // Returns "[protected]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protected session management
|
|
||||||
protectedSessionService.isProtectedSessionAvailable() // Check session
|
|
||||||
protectedSessionService.startProtectedSession() // After password entry
|
|
||||||
```
|
|
||||||
|
|
||||||
**Session timeout**: Protected sessions expire after inactivity. The encryption key is kept in memory only.
|
|
||||||
|
|
||||||
### Attribute Inheritance Patterns
|
|
||||||
Attributes can be inherited through three mechanisms:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1. Standard inheritance (#hidePromotedAttributes ~hidePromotedAttributes)
|
|
||||||
note.getInheritableAttributes() // Walks up parent tree
|
|
||||||
|
|
||||||
// 2. Child prefix inheritance (child:label copies to children)
|
|
||||||
parentNote.setLabel("child:icon", "book") // All children inherit this
|
|
||||||
|
|
||||||
// 3. Template relation inheritance (#template=templateNoteId)
|
|
||||||
note.setRelation("template", templateNoteId)
|
|
||||||
note.getInheritedAttributes() // Includes template's inheritable attributes
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cycle prevention**: Inheritance tracking prevents infinite loops when notes reference each other.
|
|
||||||
|
|
||||||
### Widget-Based UI Architecture
|
|
||||||
All UI components extend from widget base classes (`apps/client/src/widgets/`):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Right panel widget (sidebar)
|
|
||||||
class MyWidget extends RightPanelWidget {
|
|
||||||
get position() { return 100; } // Order in panel
|
|
||||||
get parentWidget() { return 'right-pane'; }
|
|
||||||
isEnabled() { return this.note && this.note.hasLabel('myLabel'); }
|
|
||||||
async refreshWithNote(note) { /* Update UI */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note-aware widget (responds to note changes)
|
|
||||||
class MyNoteWidget extends NoteContextAwareWidget {
|
|
||||||
async refreshWithNote(note) { /* Refresh when note changes */ }
|
|
||||||
async entitiesReloadedEvent({loadResults}) { /* Handle entity updates */ }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Widgets use jQuery (`this.$widget`) for DOM manipulation. Don't mix React patterns here.
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Running & Testing
|
|
||||||
```bash
|
|
||||||
# From root directory
|
|
||||||
pnpm install # Install dependencies
|
|
||||||
corepack enable # Enable pnpm if not available
|
|
||||||
pnpm server:start # Dev server (http://localhost:8080)
|
|
||||||
pnpm server:start-prod # Production mode server
|
|
||||||
pnpm desktop:start # Desktop app development
|
|
||||||
pnpm server:test spec/etapi/search.spec.ts # Run specific test
|
|
||||||
pnpm test:parallel # Client tests (can run parallel)
|
|
||||||
pnpm test:sequential # Server tests (sequential due to shared DB)
|
|
||||||
pnpm test:all # All tests (parallel + sequential)
|
|
||||||
pnpm coverage # Generate coverage reports
|
|
||||||
pnpm typecheck # Type check all projects
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
|
||||||
```bash
|
|
||||||
pnpm client:build # Build client application
|
|
||||||
pnpm server:build # Build server application
|
|
||||||
pnpm desktop:build # Build desktop application
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Organization
|
|
||||||
- **Server tests** (`apps/server/spec/`): Must run sequentially (shared database state)
|
|
||||||
- **Client tests** (`apps/client/src/`): Can run in parallel
|
|
||||||
- **E2E tests** (`apps/server-e2e/`): Use Playwright for integration testing
|
|
||||||
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
|
|
||||||
|
|
||||||
**Pattern**: When adding new API endpoints, add tests in `spec/etapi/` following existing patterns (see `search.spec.ts`).
|
|
||||||
|
|
||||||
### Monorepo Navigation
|
|
||||||
```
|
|
||||||
apps/
|
|
||||||
client/ # Frontend (shared by server & desktop)
|
|
||||||
server/ # Node.js backend with REST API
|
|
||||||
desktop/ # Electron wrapper
|
|
||||||
web-clipper/ # Browser extension for saving web content
|
|
||||||
db-compare/ # Database comparison tool
|
|
||||||
dump-db/ # Database export utility
|
|
||||||
edit-docs/ # Documentation editing tools
|
|
||||||
packages/
|
|
||||||
commons/ # Shared types and utilities
|
|
||||||
ckeditor5/ # Custom rich text editor with Trilium-specific plugins
|
|
||||||
codemirror/ # Code editor integration
|
|
||||||
highlightjs/ # Syntax highlighting
|
|
||||||
share-theme/ # Theme for shared/published notes
|
|
||||||
ckeditor5-admonition/ # Admonition blocks plugin
|
|
||||||
ckeditor5-footnotes/ # Footnotes plugin
|
|
||||||
ckeditor5-math/ # Math equations plugin
|
|
||||||
ckeditor5-mermaid/ # Mermaid diagrams plugin
|
|
||||||
```
|
|
||||||
|
|
||||||
**Filter commands**: Use `pnpm --filter server test` to run commands in specific packages.
|
|
||||||
|
|
||||||
## Critical Code Patterns
|
|
||||||
|
|
||||||
### ETAPI Backwards Compatibility
|
|
||||||
When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), maintain backwards compatibility by checking if new params exist before changing response format.
|
|
||||||
|
|
||||||
**Pattern**: ETAPI consumers expect specific response shapes. Always check for breaking changes.
|
|
||||||
|
|
||||||
### Frontend-Backend Communication
|
|
||||||
- **REST API**: `apps/server/src/routes/api/` - Internal endpoints (no auth required when `noAuthentication=true`)
|
|
||||||
- **ETAPI**: `apps/server/src/etapi/` - External API with authentication
|
|
||||||
- **WebSocket**: Real-time sync via `apps/server/src/services/ws.ts`
|
|
||||||
|
|
||||||
**Auth note**: ETAPI uses basic auth with tokens. Internal API endpoints trust the frontend.
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
- Add scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
|
|
||||||
- Update schema in `apps/server/src/assets/db/schema.sql`
|
|
||||||
- Never bypass Becca cache after migrations
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
1. **Never bypass the cache layers** - Always use `becca.notes[noteId]`, `froca.getNote()`, or equivalent cache methods. Direct database queries will cause sync issues between Becca/Froca/Shaca and won't create EntityChange records needed for synchronization.
|
|
||||||
|
|
||||||
2. **Protected notes require session check** - Before accessing `note.title` or `note.getContent()` on protected notes, check `note.isContentAvailable()` or use `note.getTitleOrProtected()` which handles this automatically.
|
|
||||||
|
|
||||||
3. **Widget lifecycle matters** - Override `refreshWithNote()` for note changes, `doRenderBody()` for initial render, `entitiesReloadedEvent()` for entity updates. Widgets use jQuery (`this.$widget`) - don't mix React patterns.
|
|
||||||
|
|
||||||
4. **Tests run differently** - Server tests must run sequentially (shared database state), client tests can run in parallel. Use `pnpm test:sequential` for backend, `pnpm test:parallel` for frontend.
|
|
||||||
|
|
||||||
5. **ETAPI requires authentication** - ETAPI endpoints use basic auth with tokens. Internal API endpoints (`apps/server/src/routes/api/`) trust the frontend when `noAuthentication=true`.
|
|
||||||
|
|
||||||
6. **Search expressions are evaluated in memory** - The search service loads all matching notes, scores them in JavaScript, then sorts. You cannot add SQL-level LIMIT/OFFSET without losing scoring functionality.
|
|
||||||
|
|
||||||
7. **Documentation edits have rules** - `docs/Script API/` is auto-generated (never edit directly). `docs/User Guide/` should be edited via `pnpm edit-docs:edit-docs`, not manually. Only `docs/Developer Guide/` and `docs/Release Notes/` are safe for direct Markdown editing.
|
|
||||||
|
|
||||||
8. **pnpm workspace filtering** - Use `pnpm --filter server <command>` or shorthand `pnpm server:test` defined in root `package.json`. Note the `--filter` syntax, not `-F` or other shortcuts.
|
|
||||||
|
|
||||||
9. **Event subscription cleanup** - When subscribing to events in widgets, unsubscribe in `cleanup()` or `doDestroy()` to prevent memory leaks.
|
|
||||||
|
|
||||||
10. **Attribute inheritance can be complex** - When checking for labels/relations, use `note.getOwnedAttribute()` for direct attributes or `note.getAttribute()` for inherited ones. Don't assume attributes are directly on the note.
|
|
||||||
|
|
||||||
## TypeScript Configuration
|
|
||||||
|
|
||||||
- **Project references**: Monorepo uses TypeScript project references (`tsconfig.json`)
|
|
||||||
- **Path mapping**: Use relative imports, not path aliases
|
|
||||||
- **Build order**: `pnpm typecheck` builds all projects in dependency order
|
|
||||||
- **Build system**: Uses Vite for fast development, ESBuild for production optimization
|
|
||||||
- **Patches**: Custom patches in `patches/` directory for CKEditor and other dependencies
|
|
||||||
|
|
||||||
## Key Files for Context
|
|
||||||
|
|
||||||
- `apps/server/src/becca/entities/bnote.ts` - Note entity methods
|
|
||||||
- `apps/client/src/services/froca.ts` - Frontend cache API
|
|
||||||
- `apps/server/src/services/search/services/search.ts` - Search implementation
|
|
||||||
- `apps/server/src/routes/routes.ts` - API route registration
|
|
||||||
- `apps/client/src/widgets/basic_widget.ts` - Widget base class
|
|
||||||
- `apps/server/src/main.ts` - Server startup entry point
|
|
||||||
- `apps/client/src/desktop.ts` - Client initialization
|
|
||||||
- `apps/server/src/services/backend_script_api.ts` - Scripting API
|
|
||||||
- `apps/server/src/assets/db/schema.sql` - Database schema
|
|
||||||
|
|
||||||
## Note Types and Features
|
|
||||||
|
|
||||||
Trilium supports multiple note types with specialized widgets in `apps/client/src/widgets/type_widgets/`:
|
|
||||||
- **Text**: Rich text with CKEditor5 (markdown import/export)
|
|
||||||
- **Code**: Syntax-highlighted code editing with CodeMirror
|
|
||||||
- **File**: Binary file attachments
|
|
||||||
- **Image**: Image display with editing capabilities
|
|
||||||
- **Canvas**: Drawing/diagramming with Excalidraw
|
|
||||||
- **Mermaid**: Diagram generation
|
|
||||||
- **Relation Map**: Visual note relationship mapping
|
|
||||||
- **Web View**: Embedded web pages
|
|
||||||
- **Doc/Book**: Hierarchical documentation structure
|
|
||||||
|
|
||||||
### Collections
|
|
||||||
Notes can be marked with the `#collection` label to enable collection view modes. Collections support multiple view types:
|
|
||||||
- **List**: Standard list view
|
|
||||||
- **Grid**: Card/grid layout
|
|
||||||
- **Calendar**: Calendar-based view
|
|
||||||
- **Table**: Tabular data view
|
|
||||||
- **GeoMap**: Geographic map view
|
|
||||||
- **Board**: Kanban-style board
|
|
||||||
- **Presentation**: Slideshow presentation mode
|
|
||||||
|
|
||||||
View types are configured via `#viewType` label (e.g., `#viewType=table`). Each view mode stores its configuration in a separate attachment (e.g., `table.json`). Collections are organized separately from regular note type templates in the note creation menu.
|
|
||||||
|
|
||||||
## Common Development Tasks
|
|
||||||
|
|
||||||
### Adding New Note Types
|
|
||||||
1. Create widget in `apps/client/src/widgets/type_widgets/`
|
|
||||||
2. Register in `apps/client/src/services/note_types.ts`
|
|
||||||
3. Add backend handling in `apps/server/src/services/notes.ts`
|
|
||||||
|
|
||||||
### Extending Search
|
|
||||||
- Search expressions handled in `apps/server/src/services/search/`
|
|
||||||
- Add new search operators in search context files
|
|
||||||
- Remember: scoring happens in-memory, not at database level
|
|
||||||
|
|
||||||
### Custom CKEditor Plugins
|
|
||||||
- Create new package in `packages/` following existing plugin structure
|
|
||||||
- Register in `packages/ckeditor5/src/plugins.ts`
|
|
||||||
- See `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid` for examples
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
- Add migration scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
|
|
||||||
- Update schema in `apps/server/src/assets/db/schema.sql`
|
|
||||||
- Never bypass Becca cache after migrations
|
|
||||||
|
|
||||||
## Security & Features
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
- Per-note encryption with granular protected sessions
|
|
||||||
- CSRF protection for API endpoints
|
|
||||||
- OpenID and TOTP authentication support
|
|
||||||
- Sanitization of user-generated content
|
|
||||||
|
|
||||||
### Scripting System
|
|
||||||
Trilium provides powerful user scripting capabilities:
|
|
||||||
- **Frontend scripts**: Run in browser context with UI access
|
|
||||||
- **Backend scripts**: Run in Node.js context with full API access
|
|
||||||
- Script API documentation in `docs/Script API/`
|
|
||||||
- Backend API available via `api` object in script context
|
|
||||||
|
|
||||||
### Internationalization
|
|
||||||
- Translation files in `apps/client/src/translations/`
|
|
||||||
- Use translation system via `t()` function
|
|
||||||
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
|
|
||||||
|
|
||||||
## Testing Conventions
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ETAPI test pattern
|
|
||||||
describe("etapi/feature", () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
config.General.noAuthentication = false;
|
|
||||||
app = await buildApp();
|
|
||||||
token = await login(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should test feature", async () => {
|
|
||||||
const response = await supertest(app)
|
|
||||||
.get("/etapi/notes?search=test")
|
|
||||||
.auth(USER, token, { type: "basic" })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.results).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Questions to Verify Understanding
|
|
||||||
|
|
||||||
Before implementing significant changes, confirm:
|
|
||||||
- Is this touching the cache layer? (Becca/Froca/Shaca must stay in sync via EntityChange records)
|
|
||||||
- Does this change API response shape? (Check backwards compatibility for ETAPI)
|
|
||||||
- Are you adding search features? (Understand expression-based architecture and in-memory scoring first)
|
|
||||||
- Is this a new widget? (Know which base class and lifecycle methods to use)
|
|
||||||
- Does this involve protected notes? (Check `isContentAvailable()` before accessing content)
|
|
||||||
- Is this a long-running operation? (Use TaskContext for progress reporting)
|
|
||||||
- Are you working with attributes? (Understand inheritance patterns: direct, child-prefix, template)
|
|
||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -95,6 +95,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
83
.github/workflows/deploy-docs.yml
vendored
83
.github/workflows/deploy-docs.yml
vendored
@@ -1,4 +1,6 @@
|
|||||||
name: Deploy Documentation
|
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
|
||||||
|
# This workflow builds and deploys your MkDocs site when changes are pushed to main
|
||||||
|
name: Deploy MkDocs Documentation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Trigger on push to main branch
|
# Trigger on push to main branch
|
||||||
@@ -9,9 +11,11 @@ on:
|
|||||||
# Only run when docs files change
|
# Only run when docs files change
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'apps/edit-docs/**'
|
- 'README.md' # README is synced to docs/index.md
|
||||||
- 'apps/build-docs/**'
|
- 'mkdocs.yml'
|
||||||
- 'packages/share-theme/**'
|
- 'requirements-docs.txt'
|
||||||
|
- '.github/workflows/deploy-docs.yml'
|
||||||
|
- 'scripts/fix-mkdocs-structure.ts'
|
||||||
|
|
||||||
# Allow manual triggering from Actions tab
|
# Allow manual triggering from Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -23,13 +27,15 @@ on:
|
|||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'apps/edit-docs/**'
|
- 'README.md' # README is synced to docs/index.md
|
||||||
- 'apps/build-docs/**'
|
- 'mkdocs.yml'
|
||||||
- 'packages/share-theme/**'
|
- 'requirements-docs.txt'
|
||||||
|
- '.github/workflows/deploy-docs.yml'
|
||||||
|
- 'scripts/fix-mkdocs-structure.ts'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
name: Build and Deploy Documentation
|
name: Build and Deploy MkDocs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
@@ -42,28 +48,73 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
cache: 'pip'
|
||||||
|
cache-dependency-path: 'requirements-docs.txt'
|
||||||
|
|
||||||
|
- name: Install MkDocs and Dependencies
|
||||||
|
run: |
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements-docs.txt
|
||||||
|
env:
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
||||||
|
|
||||||
|
# Setup pnpm before fixing docs structure
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
# Setup Node.js with pnpm
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '22'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
# Install Node.js dependencies for the TypeScript script
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Trigger build of documentation
|
- name: Fix Documentation Structure
|
||||||
run: pnpm docs:build
|
run: |
|
||||||
|
# Fix duplicate navigation entries by moving overview pages to index.md
|
||||||
|
pnpm run chore:fix-mkdocs-structure
|
||||||
|
|
||||||
|
- name: Build MkDocs Site
|
||||||
|
run: |
|
||||||
|
# Build with strict mode but allow expected warnings
|
||||||
|
mkdocs build --verbose || {
|
||||||
|
EXIT_CODE=$?
|
||||||
|
# Check if the only issue is expected warnings
|
||||||
|
if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
|
||||||
|
[ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
|
||||||
|
echo "✅ Build succeeded with expected warnings"
|
||||||
|
mkdocs build --verbose
|
||||||
|
else
|
||||||
|
echo "❌ Build failed with unexpected errors"
|
||||||
|
exit $EXIT_CODE
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Fix HTML Links
|
||||||
|
run: |
|
||||||
|
# Remove .md extensions from links in generated HTML
|
||||||
|
pnpm tsx ./scripts/fix-html-links.ts site
|
||||||
|
|
||||||
- name: Validate Built Site
|
- name: Validate Built Site
|
||||||
run: |
|
run: |
|
||||||
|
# Basic validation that important files exist
|
||||||
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
|
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
|
||||||
test -f site/developer-guide/index.html || (echo "ERROR: site/developer-guide/index.html not found" && exit 1)
|
test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
|
||||||
echo "✓ User Guide and Developer Guide built successfully"
|
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
|
||||||
|
echo "✅ Site validation passed"
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: ./.github/actions/deploy-to-cloudflare-pages
|
uses: ./.github/actions/deploy-to-cloudflare-pages
|
||||||
|
|||||||
10
.github/workflows/dev.yml
vendored
10
.github/workflows/dev.yml
vendored
@@ -24,13 +24,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- test_dev
|
- test_dev
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
28
.github/workflows/main-docker.yml
vendored
28
.github/workflows/main-docker.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set IMAGE_NAME to lowercase
|
- name: Set IMAGE_NAME to lowercase
|
||||||
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
||||||
@@ -44,9 +44,9 @@ jobs:
|
|||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install npm dependencies
|
- name: Install npm dependencies
|
||||||
@@ -86,12 +86,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright trace
|
- name: Upload Playwright trace
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Playwright trace (${{ matrix.dockerfile }})
|
name: Playwright trace (${{ matrix.dockerfile }})
|
||||||
path: test-output/playwright/output
|
path: test-output/playwright/output
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v5
|
- uses: actions/upload-artifact@v4
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
name: Playwright report (${{ matrix.dockerfile }})
|
name: Playwright report (${{ matrix.dockerfile }})
|
||||||
@@ -116,10 +116,10 @@ jobs:
|
|||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
image: ubuntu-24.04-arm
|
image: ubuntu-24.04-arm
|
||||||
- dockerfile: Dockerfile.legacy
|
- dockerfile: Dockerfile
|
||||||
platform: linux/arm/v7
|
platform: linux/arm/v7
|
||||||
image: ubuntu-24.04-arm
|
image: ubuntu-24.04-arm
|
||||||
- dockerfile: Dockerfile.legacy
|
- dockerfile: Dockerfile
|
||||||
platform: linux/arm/v8
|
platform: linux/arm/v8
|
||||||
image: ubuntu-24.04-arm
|
image: ubuntu-24.04-arm
|
||||||
runs-on: ${{ matrix.image }}
|
runs-on: ${{ matrix.image }}
|
||||||
@@ -141,12 +141,12 @@ jobs:
|
|||||||
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
|
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -155,10 +155,6 @@ jobs:
|
|||||||
- name: Update build info
|
- name: Update build info
|
||||||
run: pnpm run chore:update-build-info
|
run: pnpm run chore:update-build-info
|
||||||
|
|
||||||
- name: Update nightly version
|
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
run: pnpm run chore:ci-update-nightly-version
|
|
||||||
|
|
||||||
- name: Run the TypeScript build
|
- name: Run the TypeScript build
|
||||||
run: pnpm run server:build
|
run: pnpm run server:build
|
||||||
|
|
||||||
@@ -213,7 +209,7 @@ jobs:
|
|||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
@@ -227,7 +223,7 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
|
|||||||
29
.github/workflows/nightly.yml
vendored
29
.github/workflows/nightly.yml
vendored
@@ -45,32 +45,19 @@ jobs:
|
|||||||
image: win-signing
|
image: win-signing
|
||||||
shell: cmd
|
shell: cmd
|
||||||
forge_platform: win32
|
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 }}
|
runs-on: ${{ matrix.os.image }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Update nightly version
|
- name: Update nightly version
|
||||||
run: pnpm run chore:ci-update-nightly-version
|
run: npm run chore:ci-update-nightly-version
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-electron
|
uses: ./.github/actions/build-electron
|
||||||
with:
|
with:
|
||||||
@@ -90,7 +77,7 @@ jobs:
|
|||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.4.2
|
uses: softprops/action-gh-release@v2.3.4
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
@@ -102,7 +89,7 @@ jobs:
|
|||||||
name: Nightly Build
|
name: Nightly Build
|
||||||
|
|
||||||
- name: Publish artifacts
|
- name: Publish artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
||||||
@@ -122,7 +109,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-server
|
uses: ./.github/actions/build-server
|
||||||
@@ -131,7 +118,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.4.2
|
uses: softprops/action-gh-release@v2.3.4
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
|
|||||||
64
.github/workflows/playwright.yml
vendored
64
.github/workflows/playwright.yml
vendored
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- hotfix
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "apps/website/**"
|
- "apps/website/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -14,74 +13,29 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
main:
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- name: linux-x64
|
|
||||||
os: ubuntu-22.04
|
|
||||||
arch: x64
|
|
||||||
- name: linux-arm64
|
|
||||||
os: ubuntu-24.04-arm
|
|
||||||
arch: arm64
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
name: E2E tests on ${{ matrix.name }}
|
|
||||||
env:
|
|
||||||
TRILIUM_DOCKER: 1
|
|
||||||
TRILIUM_PORT: 8082
|
|
||||||
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
|
||||||
TRILIUM_INTEGRATION_TEST: memory
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
filter: tree:0
|
filter: tree:0
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm exec playwright install --with-deps
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- run: pnpm --filter server-e2e e2e
|
||||||
run: pnpm exec playwright install --with-deps
|
|
||||||
|
|
||||||
- name: Build the server
|
|
||||||
uses: ./.github/actions/build-server
|
|
||||||
with:
|
|
||||||
os: linux
|
|
||||||
arch: ${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Unpack and start the server
|
|
||||||
run: |
|
|
||||||
version=$(node --eval "console.log(require('./package.json').version)")
|
|
||||||
file=$(find ./upload -name '*.tar.xz' -print -quit)
|
|
||||||
name=$(basename "$file" .tar.xz)
|
|
||||||
mkdir -p ./server-dist
|
|
||||||
tar -xvf "$file" -C ./server-dist
|
|
||||||
server_dir="./server-dist/TriliumNotes-Server-$version-linux-${{ matrix.arch }}"
|
|
||||||
if [ ! -d "$server_dir" ]; then
|
|
||||||
echo Missing dir.
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cd "$server_dir"
|
|
||||||
"./trilium.sh" &
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
- name: Server end-to-end tests
|
|
||||||
run: pnpm --filter server-e2e e2e
|
|
||||||
|
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: e2e report ${{ matrix.arch }}
|
name: e2e report
|
||||||
path: apps/server-e2e/test-output
|
path: apps/server-e2e/test-output
|
||||||
|
|
||||||
- name: Kill the server
|
|
||||||
if: always()
|
|
||||||
run: pkill -f trilium || true
|
|
||||||
|
|||||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -45,12 +45,12 @@ jobs:
|
|||||||
forge_platform: linux
|
forge_platform: linux
|
||||||
runs-on: ${{ matrix.os.image }}
|
runs-on: ${{ matrix.os.image }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Upload the artifact
|
- name: Upload the artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
||||||
path: apps/desktop/upload/*.*
|
path: apps/desktop/upload/*.*
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-server
|
uses: ./.github/actions/build-server
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload the artifact
|
- name: Upload the artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-server-linux-${{ matrix.arch }}
|
name: release-server-linux-${{ matrix.arch }}
|
||||||
path: upload/*.*
|
path: upload/*.*
|
||||||
@@ -114,20 +114,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: mkdir upload
|
- run: mkdir upload
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
docs/Release Notes
|
docs/Release Notes
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
pattern: release-*
|
pattern: release-*
|
||||||
path: upload
|
path: upload
|
||||||
|
|
||||||
- name: Publish stable release
|
- name: Publish stable release
|
||||||
uses: softprops/action-gh-release@v2.4.2
|
uses: softprops/action-gh-release@v2.3.4
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: false
|
||||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||||
|
|||||||
6
.github/workflows/website.yml
vendored
6
.github/workflows/website.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
|||||||
pull-requests: write # For PR preview comments
|
pull-requests: write # For PR preview comments
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 22
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
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,
|
# the `{key}` will be placed by a proper keypath matching regex,
|
||||||
# you can ignore it and use your own matching rules as well
|
# you can ignore it and use your own matching rules as well
|
||||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
- "[^\\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
|
# 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
|
# and works like how the i18next framework identifies the namespace scope from the
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -5,8 +5,7 @@
|
|||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"i18n-ally.localesPaths": [
|
"i18n-ally.localesPaths": [
|
||||||
"apps/server/src/assets/translations",
|
"apps/server/src/assets/translations",
|
||||||
"apps/client/src/translations",
|
"apps/client/src/translations"
|
||||||
"apps/website/public/translations"
|
|
||||||
],
|
],
|
||||||
"npm.exclude": [
|
"npm.exclude": [
|
||||||
"**/dist",
|
"**/dist",
|
||||||
|
|||||||
15
README.md
15
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
|
# Trilium Notes
|
||||||
|
|
||||||
 
|
 
|
||||||
@@ -24,10 +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>
|
<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
|
## 📚 Documentation
|
||||||
|
|
||||||
**Visit our comprehensive documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
**Visit our comprehensive documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
||||||
|
|||||||
@@ -35,20 +35,22 @@
|
|||||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.57.0",
|
"@playwright/test": "1.56.0",
|
||||||
"@stylistic/eslint-plugin": "5.6.1",
|
"@stylistic/eslint-plugin": "5.4.0",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.3",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "22.18.8",
|
||||||
"@types/yargs": "17.0.35",
|
"@types/yargs": "17.0.33",
|
||||||
"@vitest/coverage-v8": "4.0.14",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.37.0",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"jsdoc": "4.0.5",
|
"jsdoc": "4.0.4",
|
||||||
"lorem-ipsum": "2.0.8",
|
"lorem-ipsum": "2.0.8",
|
||||||
"rcedit": "5.0.2",
|
"rcedit": "4.0.1",
|
||||||
"rimraf": "6.1.2",
|
"rimraf": "6.0.1",
|
||||||
"tslib": "2.8.1"
|
"tslib": "2.8.1",
|
||||||
|
"typedoc": "0.28.13",
|
||||||
|
"typedoc-plugin-missing-exports": "4.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "0.6.6"
|
"appdmg": "0.6.6"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type child_process from "child_process";
|
||||||
import { describe, beforeAll, afterAll } from "vitest";
|
import { describe, beforeAll, afterAll } from "vitest";
|
||||||
|
|
||||||
let etapiAuthToken: string | undefined;
|
let etapiAuthToken: string | undefined;
|
||||||
@@ -11,6 +12,8 @@ type SpecDefinitionsFunc = () => void;
|
|||||||
|
|
||||||
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
|
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
|
||||||
describe(description, () => {
|
describe(description, () => {
|
||||||
|
let appProcess: ReturnType<typeof child_process.spawn>;
|
||||||
|
|
||||||
beforeAll(async () => {});
|
beforeAll(async () => {});
|
||||||
|
|
||||||
afterAll(() => {});
|
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.24.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@redocly/cli": "2.12.0",
|
|
||||||
"archiver": "7.0.1",
|
|
||||||
"fs-extra": "11.3.2",
|
|
||||||
"react": "19.2.0",
|
|
||||||
"react-dom": "19.2.0",
|
|
||||||
"typedoc": "0.28.15",
|
|
||||||
"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,147 +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 importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
|
||||||
const note = await importData(sourcePath);
|
|
||||||
|
|
||||||
// Use a meaningful name for the temporary zip file
|
|
||||||
const zipName = outputSubDir || "user-guide";
|
|
||||||
const zipFilePath = `output-${zipName}.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);
|
|
||||||
|
|
||||||
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
|
|
||||||
const outputPath = outputSubDir ? join(OUTPUT_DIR, outputSubDir) : OUTPUT_DIR;
|
|
||||||
await extractZip(zipFilePath, outputPath);
|
|
||||||
} finally {
|
|
||||||
if (await fsExtra.exists(zipFilePath)) {
|
|
||||||
await fsExtra.rm(zipFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Wait for becca to be loaded before importing data
|
|
||||||
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
|
|
||||||
await beccaLoader.beccaLoaded;
|
|
||||||
|
|
||||||
// Build User Guide
|
|
||||||
console.log("Building User Guide...");
|
|
||||||
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
|
|
||||||
|
|
||||||
// Build Developer Guide
|
|
||||||
console.log("Building Developer Guide...");
|
|
||||||
await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide");
|
|
||||||
|
|
||||||
// Copy favicon.
|
|
||||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
|
|
||||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico"));
|
|
||||||
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
|
|
||||||
|
|
||||||
console.log("Documentation built successfully!");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function importData(path: string) {
|
|
||||||
const buffer = await createImportZip(path);
|
|
||||||
const importService = (await import("../../server/src/services/import/zip.js")).default;
|
|
||||||
const TaskContext = (await import("../../server/src/services/task_context.js")).default;
|
|
||||||
const context = new TaskContext("no-progress-reporting", "importNotes", null);
|
|
||||||
const becca = (await import("../../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,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0; url=/user-guide">
|
|
||||||
<title>Redirecting...</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>If you are not redirected automatically, <a href="/user-guide">click here</a>.</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { join } from "path";
|
|
||||||
import BuildContext from "./context";
|
|
||||||
import buildSwagger from "./swagger";
|
|
||||||
import { cpSync, 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);
|
|
||||||
|
|
||||||
// Copy index and 404 files.
|
|
||||||
cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html"));
|
|
||||||
cpSync(join(context.baseDir, "user-guide/404.html"), join(context.baseDir, "404.html"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@triliumnext/client",
|
"name": "@triliumnext/client",
|
||||||
"version": "0.100.0",
|
"version": "0.99.1",
|
||||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "9.39.1",
|
"@eslint/js": "9.37.0",
|
||||||
"@excalidraw/excalidraw": "0.18.0",
|
"@excalidraw/excalidraw": "0.18.0",
|
||||||
"@fullcalendar/core": "6.1.19",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
@@ -25,41 +25,40 @@
|
|||||||
"@fullcalendar/timegrid": "6.1.19",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||||
"@mermaid-js/layout-elk": "0.2.0",
|
"@mermaid-js/layout-elk": "0.2.0",
|
||||||
"@mind-elixir/node-menu": "5.0.1",
|
"@mind-elixir/node-menu": "5.0.0",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@triliumnext/ckeditor5": "workspace:*",
|
"@triliumnext/ckeditor5": "workspace:*",
|
||||||
"@triliumnext/codemirror": "workspace:*",
|
"@triliumnext/codemirror": "workspace:*",
|
||||||
"@triliumnext/commons": "workspace:*",
|
"@triliumnext/commons": "workspace:*",
|
||||||
"@triliumnext/highlightjs": "workspace:*",
|
"@triliumnext/highlightjs": "workspace:*",
|
||||||
"@triliumnext/share-theme": "workspace:*",
|
"@triliumnext/share-theme": "workspace:*",
|
||||||
"@triliumnext/split.js": "workspace:*",
|
|
||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.8",
|
"bootstrap": "5.3.8",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"clsx": "2.1.1",
|
"dayjs": "1.11.18",
|
||||||
"color": "5.0.3",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
"debounce": "3.0.0",
|
"debounce": "2.2.0",
|
||||||
"draggabilly": "3.0.0",
|
"draggabilly": "3.0.0",
|
||||||
"force-graph": "1.51.0",
|
"force-graph": "1.51.0",
|
||||||
"globals": "16.5.0",
|
"globals": "16.4.0",
|
||||||
"i18next": "25.6.3",
|
"i18next": "25.5.3",
|
||||||
"i18next-http-backend": "3.0.2",
|
"i18next-http-backend": "3.0.2",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery.fancytree": "2.38.5",
|
"jquery.fancytree": "2.38.5",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.25",
|
"katex": "0.16.23",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-gpx": "2.2.0",
|
"leaflet-gpx": "2.2.0",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "17.0.1",
|
"marked": "16.3.0",
|
||||||
"mermaid": "11.12.1",
|
"mermaid": "11.12.0",
|
||||||
"mind-elixir": "5.3.7",
|
"mind-elixir": "5.1.1",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"react-i18next": "16.3.5",
|
"react-i18next": "16.0.0",
|
||||||
"reveal.js": "5.2.1",
|
"split.js": "1.6.5",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.3.1",
|
"tabulator-tables": "6.3.1",
|
||||||
"vanilla-js-wheel-zoom": "9.0.4"
|
"vanilla-js-wheel-zoom": "9.0.4"
|
||||||
@@ -69,14 +68,13 @@
|
|||||||
"@preact/preset-vite": "2.10.2",
|
"@preact/preset-vite": "2.10.2",
|
||||||
"@types/bootstrap": "5.2.10",
|
"@types/bootstrap": "5.2.10",
|
||||||
"@types/jquery": "3.5.33",
|
"@types/jquery": "3.5.33",
|
||||||
"@types/leaflet": "1.9.21",
|
"@types/leaflet": "1.9.20",
|
||||||
"@types/leaflet-gpx": "1.3.8",
|
"@types/leaflet-gpx": "1.3.8",
|
||||||
"@types/mark.js": "8.11.12",
|
"@types/mark.js": "8.11.12",
|
||||||
"@types/reveal.js": "5.2.1",
|
"@types/tabulator-tables": "6.2.11",
|
||||||
"@types/tabulator-tables": "6.3.0",
|
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"happy-dom": "20.0.11",
|
"happy-dom": "19.0.2",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
"vite-plugin-static-copy": "3.1.4"
|
"vite-plugin-static-copy": "3.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ import MainTreeExecutors from "./main_tree_executors.js";
|
|||||||
import toast from "../services/toast.js";
|
import toast from "../services/toast.js";
|
||||||
import ShortcutComponent from "./shortcut_component.js";
|
import ShortcutComponent from "./shortcut_component.js";
|
||||||
import { t, initLocale } from "../services/i18n.js";
|
import { t, initLocale } from "../services/i18n.js";
|
||||||
|
import type NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||||
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
||||||
@@ -20,6 +21,8 @@ import type LoadResults from "../services/load_results.js";
|
|||||||
import type { Attribute } from "../services/attribute_parser.js";
|
import type { Attribute } from "../services/attribute_parser.js";
|
||||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
|
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
|
||||||
|
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
|
||||||
|
import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js";
|
||||||
import type { NativeImage, TouchBar } from "electron";
|
import type { NativeImage, TouchBar } from "electron";
|
||||||
import TouchBarComponent from "./touch_bar.js";
|
import TouchBarComponent from "./touch_bar.js";
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
@@ -30,10 +33,6 @@ import { ColumnComponent } from "tabulator-tables";
|
|||||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||||
import type RootContainer from "../widgets/containers/root_container.js";
|
import type RootContainer from "../widgets/containers/root_container.js";
|
||||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||||
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
|
||||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
|
||||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
|
||||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||||
@@ -200,7 +199,7 @@ export type CommandMappings = {
|
|||||||
resetLauncher: ContextMenuCommandData;
|
resetLauncher: ContextMenuCommandData;
|
||||||
|
|
||||||
executeInActiveNoteDetailWidget: CommandData & {
|
executeInActiveNoteDetailWidget: CommandData & {
|
||||||
callback: (value: ReactWrappedWidget) => void;
|
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
|
||||||
};
|
};
|
||||||
executeWithTextEditor: CommandData &
|
executeWithTextEditor: CommandData &
|
||||||
ExecuteCommandData<CKTextEditor> & {
|
ExecuteCommandData<CKTextEditor> & {
|
||||||
@@ -212,19 +211,19 @@ export type CommandMappings = {
|
|||||||
* Generally should not be invoked manually, as it is used by {@link NoteContext.getContentElement}.
|
* Generally should not be invoked manually, as it is used by {@link NoteContext.getContentElement}.
|
||||||
*/
|
*/
|
||||||
executeWithContentElement: CommandData & ExecuteCommandData<JQuery<HTMLElement>>;
|
executeWithContentElement: CommandData & ExecuteCommandData<JQuery<HTMLElement>>;
|
||||||
executeWithTypeWidget: CommandData & ExecuteCommandData<ReactWrappedWidget | null>;
|
executeWithTypeWidget: CommandData & ExecuteCommandData<TypeWidget | null>;
|
||||||
addTextToActiveEditor: CommandData & {
|
addTextToActiveEditor: CommandData & {
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
/** Works only in the electron context menu. */
|
/** Works only in the electron context menu. */
|
||||||
replaceMisspelling: CommandData;
|
replaceMisspelling: CommandData;
|
||||||
|
|
||||||
|
importMarkdownInline: CommandData;
|
||||||
showPasswordNotSet: CommandData;
|
showPasswordNotSet: CommandData;
|
||||||
showProtectedSessionPasswordDialog: CommandData;
|
showProtectedSessionPasswordDialog: CommandData;
|
||||||
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
||||||
showIncludeNoteDialog: CommandData & IncludeNoteOpts;
|
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
|
||||||
showAddLinkDialog: CommandData & AddLinkOpts;
|
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
|
||||||
showPasteMarkdownDialog: CommandData & MarkdownImportOpts;
|
|
||||||
closeProtectedSessionPasswordDialog: CommandData;
|
closeProtectedSessionPasswordDialog: CommandData;
|
||||||
copyImageReferenceToClipboard: CommandData;
|
copyImageReferenceToClipboard: CommandData;
|
||||||
copyImageToClipboard: CommandData;
|
copyImageToClipboard: CommandData;
|
||||||
@@ -271,7 +270,6 @@ export type CommandMappings = {
|
|||||||
closeThisNoteSplit: CommandData;
|
closeThisNoteSplit: CommandData;
|
||||||
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
|
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
|
||||||
jumpToNote: CommandData;
|
jumpToNote: CommandData;
|
||||||
openTodayNote: CommandData;
|
|
||||||
commandPalette: CommandData;
|
commandPalette: CommandData;
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
@@ -329,7 +327,6 @@ export type CommandMappings = {
|
|||||||
exportAsPdf: CommandData;
|
exportAsPdf: CommandData;
|
||||||
openNoteExternally: CommandData;
|
openNoteExternally: CommandData;
|
||||||
openNoteCustom: CommandData;
|
openNoteCustom: CommandData;
|
||||||
openNoteOnServer: CommandData;
|
|
||||||
renderActiveNote: CommandData;
|
renderActiveNote: CommandData;
|
||||||
unhoist: CommandData;
|
unhoist: CommandData;
|
||||||
reloadFrontendApp: CommandData;
|
reloadFrontendApp: CommandData;
|
||||||
@@ -445,7 +442,6 @@ type EventMappings = {
|
|||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
searchRefreshed: { ntxId?: string | null };
|
searchRefreshed: { ntxId?: string | null };
|
||||||
textEditorRefreshed: { ntxId?: string | null, editor: CKTextEditor };
|
|
||||||
hoistedNoteChanged: {
|
hoistedNoteChanged: {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
ntxId: string | null;
|
ntxId: string | null;
|
||||||
@@ -487,9 +483,14 @@ type EventMappings = {
|
|||||||
relationMapResetPanZoom: { ntxId: string | null | undefined };
|
relationMapResetPanZoom: { ntxId: string | null | undefined };
|
||||||
relationMapResetZoomIn: { ntxId: string | null | undefined };
|
relationMapResetZoomIn: { ntxId: string | null | undefined };
|
||||||
relationMapResetZoomOut: { ntxId: string | null | undefined };
|
relationMapResetZoomOut: { ntxId: string | null | undefined };
|
||||||
activeNoteChanged: {ntxId: string | null | undefined};
|
activeNoteChanged: {};
|
||||||
showAddLinkDialog: AddLinkOpts;
|
showAddLinkDialog: {
|
||||||
showIncludeDialog: IncludeNoteOpts;
|
textTypeWidget: EditableTextTypeWidget;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
showIncludeDialog: {
|
||||||
|
textTypeWidget: EditableTextTypeWidget;
|
||||||
|
};
|
||||||
openBulkActionsDialog: {
|
openBulkActionsDialog: {
|
||||||
selectedOrActiveNoteIds: string[];
|
selectedOrActiveNoteIds: string[];
|
||||||
};
|
};
|
||||||
@@ -497,10 +498,6 @@ type EventMappings = {
|
|||||||
noteIds: string[];
|
noteIds: string[];
|
||||||
};
|
};
|
||||||
refreshData: { ntxId: string | null | undefined };
|
refreshData: { ntxId: string | null | undefined };
|
||||||
contentSafeMarginChanged: {
|
|
||||||
top: number;
|
|
||||||
noteContext: NoteContext;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventListener<T extends EventNames> = {
|
export type EventListener<T extends EventNames> = {
|
||||||
@@ -668,10 +665,6 @@ export class AppContext extends Component {
|
|||||||
this.beforeUnloadListeners.push(obj);
|
this.beforeUnloadListeners.push(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeBeforeUnloadListener(listener: (() => boolean)) {
|
|
||||||
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const appContext = new AppContext(window.glob.isMainWindow);
|
const appContext = new AppContext(window.glob.isMainWindow);
|
||||||
|
|||||||
@@ -159,16 +159,6 @@ export default class Entrypoints extends Component {
|
|||||||
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
|
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() {
|
async runActiveNoteCommand() {
|
||||||
const noteContext = appContext.tabManager.getActiveContext();
|
const noteContext = appContext.tabManager.getActiveContext();
|
||||||
if (!noteContext) {
|
if (!noteContext) {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import hoistedNoteService from "../services/hoisted_note.js";
|
|||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
import type { ViewScope } from "../services/link.js";
|
import type { ViewScope } from "../services/link.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
|
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
import { closeActiveDialog } from "../services/dialog.js";
|
import { closeActiveDialog } from "../services/dialog.js";
|
||||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
|
||||||
|
|
||||||
export interface SetNoteOpts {
|
export interface SetNoteOpts {
|
||||||
triggerSwitchEvent?: unknown;
|
triggerSwitchEvent?: unknown;
|
||||||
@@ -321,21 +321,15 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.type === "search") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collections must always display a note list, even if no children.
|
// Collections must always display a note list, even if no children.
|
||||||
if (note.type === "book") {
|
|
||||||
const viewType = note.getLabelValue("viewType") ?? "grid";
|
const viewType = note.getLabelValue("viewType") ?? "grid";
|
||||||
if (!["list", "grid"].includes(viewType)) {
|
if (!["list", "grid"].includes(viewType)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!note.hasChildren()) {
|
if (!note.hasChildren()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -401,7 +395,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
|
|
||||||
async getTypeWidget() {
|
async getTypeWidget() {
|
||||||
return this.timeout(
|
return this.timeout(
|
||||||
new Promise<ReactWrappedWidget | null>((resolve) =>
|
new Promise<TypeWidget | null>((resolve) =>
|
||||||
appContext.triggerCommand("executeWithTypeWidget", {
|
appContext.triggerCommand("executeWithTypeWidget", {
|
||||||
resolve,
|
resolve,
|
||||||
ntxId: this.ntxId
|
ntxId: this.ntxId
|
||||||
@@ -444,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;
|
export default NoteContext;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import protectedSessionService from "../services/protected_session.js";
|
|||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
import LlmChatPanel from "../widgets/llm_chat_panel.js";
|
||||||
import toastService from "../services/toast.js";
|
import toastService from "../services/toast.js";
|
||||||
import noteCreateService from "../services/note_create.js";
|
import noteCreateService from "../services/note_create.js";
|
||||||
|
|
||||||
@@ -66,13 +67,6 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openNoteOnServerCommand() {
|
|
||||||
const noteId = appContext.tabManager.getActiveContextNoteId();
|
|
||||||
if (noteId) {
|
|
||||||
openService.openNoteOnServer(noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enterProtectedSessionCommand() {
|
enterProtectedSessionCommand() {
|
||||||
protectedSessionService.enterProtectedSession();
|
protectedSessionService.enterProtectedSession();
|
||||||
}
|
}
|
||||||
@@ -177,8 +171,7 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleTrayCommand() {
|
toggleTrayCommand() {
|
||||||
if (!utils.isElectron() || options.is("disableTray")) return;
|
if (!utils.isElectron()) return;
|
||||||
|
|
||||||
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
|
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
|
||||||
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
|
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
|
||||||
const isVisible = windows.every((w) => w.isVisible());
|
const isVisible = windows.every((w) => w.isVisible());
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export default class TabManager extends Component {
|
|||||||
const activeNoteContext = this.getActiveContext();
|
const activeNoteContext = this.getActiveContext();
|
||||||
this.updateDocumentTitle(activeNoteContext);
|
this.updateDocumentTitle(activeNoteContext);
|
||||||
|
|
||||||
this.triggerEvent("activeNoteChanged", {ntxId:activeNoteContext?.ntxId}); // trigger this even in on popstate event
|
this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateHash(): string {
|
calculateHash(): string {
|
||||||
@@ -265,7 +265,6 @@ export default class TabManager extends Component {
|
|||||||
mainNtxId: string | null = null
|
mainNtxId: string | null = null
|
||||||
): Promise<NoteContext> {
|
): Promise<NoteContext> {
|
||||||
const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId);
|
const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId);
|
||||||
noteContext.setEmpty();
|
|
||||||
|
|
||||||
const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId);
|
const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId);
|
||||||
|
|
||||||
@@ -647,32 +646,7 @@ export default class TabManager extends Component {
|
|||||||
...this.noteContexts.slice(-noteContexts.length),
|
...this.noteContexts.slice(-noteContexts.length),
|
||||||
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
||||||
];
|
];
|
||||||
|
this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null) });
|
||||||
// Update mainNtxId if the restored pane is the main pane in the split pane
|
|
||||||
const { oldMainNtxId, newMainNtxId } = (() => {
|
|
||||||
if (noteContexts.length !== 1) {
|
|
||||||
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainNtxId = noteContexts[0]?.mainNtxId;
|
|
||||||
const index = this.noteContexts.findIndex(c => c.ntxId === mainNtxId);
|
|
||||||
|
|
||||||
// No need to update if the restored position is after mainNtxId
|
|
||||||
if (index === -1 || lastClosedTab.position > index) {
|
|
||||||
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
oldMainNtxId: this.noteContexts[index].ntxId ?? undefined,
|
|
||||||
newMainNtxId: noteContexts[0]?.ntxId ?? undefined
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
this.triggerCommand("noteContextReorder", {
|
|
||||||
ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null),
|
|
||||||
oldMainNtxId,
|
|
||||||
newMainNtxId
|
|
||||||
});
|
|
||||||
|
|
||||||
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
||||||
if (mainNtx) {
|
if (mainNtx) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { t } from "./services/i18n.js";
|
|||||||
import options from "./services/options.js";
|
import options from "./services/options.js";
|
||||||
import type ElectronRemote from "@electron/remote";
|
import type ElectronRemote from "@electron/remote";
|
||||||
import type Electron from "electron";
|
import type Electron from "electron";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "boxicons/css/boxicons.min.css";
|
import "boxicons/css/boxicons.min.css";
|
||||||
import "autocomplete.js/index_jquery.js";
|
import "autocomplete.js/index_jquery.js";
|
||||||
|
|
||||||
@@ -58,7 +59,6 @@ function initOnElectron() {
|
|||||||
|
|
||||||
initDarkOrLightMode(style);
|
initDarkOrLightMode(style);
|
||||||
initTransparencyEffects(style, currentWindow);
|
initTransparencyEffects(style, currentWindow);
|
||||||
initFullScreenDetection(currentWindow);
|
|
||||||
|
|
||||||
if (options.get("nativeTitleBarVisible") !== "true") {
|
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||||
initTitleBarButtons(style, currentWindow);
|
initTitleBarButtons(style, currentWindow);
|
||||||
@@ -88,11 +88,6 @@ function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFullScreenDetection(currentWindow: Electron.BrowserWindow) {
|
|
||||||
currentWindow.on("enter-full-screen", () => document.body.classList.add("full-screen"));
|
|
||||||
currentWindow.on("leave-full-screen", () => document.body.classList.remove("full-screen"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||||
if (window.glob.platform === "win32") {
|
if (window.glob.platform === "win32") {
|
||||||
const material = style.getPropertyValue("--background-material");
|
const material = style.getPropertyValue("--background-material");
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
|
import ws from "../services/ws.js";
|
||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import cssClassManager from "../services/css_class_manager.js";
|
import cssClassManager from "../services/css_class_manager.js";
|
||||||
import type { Froca } from "../services/froca-interface.js";
|
import type { Froca } from "../services/froca-interface.js";
|
||||||
import type FAttachment from "./fattachment.js";
|
import type FAttachment from "./fattachment.js";
|
||||||
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import search from "../services/search.js";
|
|
||||||
|
|
||||||
const LABEL = "label";
|
const LABEL = "label";
|
||||||
const RELATION = "relation";
|
const RELATION = "relation";
|
||||||
@@ -240,7 +240,7 @@ export default class FNote {
|
|||||||
|
|
||||||
const aNote = this.froca.getNoteFromCache(aNoteId);
|
const aNote = this.froca.getNoteFromCache(aNoteId);
|
||||||
|
|
||||||
if (!aNote || aNote.isArchived || aNote.isHiddenCompletely()) {
|
if (aNote.isArchived || aNote.isHiddenCompletely()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,23 +256,6 @@ export default class FNote {
|
|||||||
return this.children;
|
return this.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildNoteIdsWithArchiveFiltering(includeArchived = false) {
|
|
||||||
const isHiddenNote = this.noteId.startsWith("_");
|
|
||||||
const isSearchNote = this.type === "search";
|
|
||||||
if (!includeArchived && !isHiddenNote && !isSearchNote) {
|
|
||||||
const unorderedIds = new Set(await search.searchForNoteIds(`note.parents.noteId="${this.noteId}" #!archived`));
|
|
||||||
const results: string[] = [];
|
|
||||||
for (const id of this.children) {
|
|
||||||
if (unorderedIds.has(id)) {
|
|
||||||
results.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
} else {
|
|
||||||
return this.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSubtreeNoteIds(includeArchived = false) {
|
async getSubtreeNoteIds(includeArchived = false) {
|
||||||
let noteIds: (string | string[])[] = [];
|
let noteIds: (string | string[])[] = [];
|
||||||
for (const child of await this.getChildNotes()) {
|
for (const child of await this.getChildNotes()) {
|
||||||
@@ -435,7 +418,7 @@ export default class FNote {
|
|||||||
return notePaths;
|
return notePaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] {
|
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
|
||||||
const isHoistedRoot = hoistedNoteId === "root";
|
const isHoistedRoot = hoistedNoteId === "root";
|
||||||
|
|
||||||
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
||||||
@@ -446,23 +429,7 @@ export default class FNote {
|
|||||||
isHidden: path.includes("_hidden")
|
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) => {
|
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) {
|
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
|
||||||
return a.isInHoistedSubTree ? -1 : 1;
|
return a.isInHoistedSubTree ? -1 : 1;
|
||||||
} else if (a.isArchived !== b.isArchived) {
|
} else if (a.isArchived !== b.isArchived) {
|
||||||
@@ -483,11 +450,10 @@ export default class FNote {
|
|||||||
* Returns the note path considered to be the "best"
|
* Returns the note path considered to be the "best"
|
||||||
*
|
*
|
||||||
* @param {string} [hoistedNoteId='root']
|
* @param {string} [hoistedNoteId='root']
|
||||||
* @param {string|null} [activeNotePath=null]
|
|
||||||
* @return {string[]} array of noteIds constituting the particular note path
|
* @return {string[]} array of noteIds constituting the particular note path
|
||||||
*/
|
*/
|
||||||
getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) {
|
getBestNotePath(hoistedNoteId = "root") {
|
||||||
return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath;
|
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -620,7 +586,7 @@ export default class FNote {
|
|||||||
let childBranches = this.getChildBranches();
|
let childBranches = this.getChildBranches();
|
||||||
|
|
||||||
if (!childBranches) {
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,16 +772,6 @@ export default class FNote {
|
|||||||
return this.getAttributeValue(LABEL, name);
|
return this.getAttributeValue(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabelOrRelation(nameWithPrefix: string) {
|
|
||||||
if (nameWithPrefix.startsWith("#")) {
|
|
||||||
return this.getLabelValue(nameWithPrefix.substring(1));
|
|
||||||
} else if (nameWithPrefix.startsWith("~")) {
|
|
||||||
return this.getRelationValue(nameWithPrefix.substring(1));
|
|
||||||
} else {
|
|
||||||
return this.getLabelValue(nameWithPrefix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @returns relation value if relation exists, null otherwise
|
* @returns relation value if relation exists, null otherwise
|
||||||
@@ -867,7 +823,8 @@ export default class FNote {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const promotedAttrs = this.getAttributeDefinitions()
|
const promotedAttrs = this.getAttributes()
|
||||||
|
.filter((attr) => attr.isDefinition())
|
||||||
.filter((attr) => {
|
.filter((attr) => {
|
||||||
const def = attr.getDefinition();
|
const def = attr.getDefinition();
|
||||||
|
|
||||||
@@ -887,11 +844,6 @@ export default class FNote {
|
|||||||
return promotedAttrs;
|
return promotedAttrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeDefinitions() {
|
|
||||||
return this.getAttributes()
|
|
||||||
.filter((attr) => attr.isDefinition());
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) {
|
hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) {
|
||||||
if (this.noteId === ancestorNoteId) {
|
if (this.noteId === ancestorNoteId) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,50 +1,47 @@
|
|||||||
import { applyModals } from "./layout_commons.js";
|
|
||||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
|
||||||
import ApiLog from "../widgets/api_log.jsx";
|
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
|
||||||
import ContentHeader from "../widgets/containers/content_header.js";
|
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
|
||||||
import FindWidget from "../widgets/find.js";
|
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
|
||||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
|
||||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
|
||||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
|
||||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
|
||||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
|
||||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
|
||||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
|
||||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
|
||||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
|
||||||
import SharedInfo from "../widgets/shared_info.jsx";
|
|
||||||
import SpacerWidget from "../widgets/spacer.js";
|
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
|
||||||
import SqlResults from "../widgets/sql_result.js";
|
|
||||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||||
|
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 NoteIconWidget from "../widgets/note_icon.jsx";
|
||||||
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
|
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||||
|
import SpacerWidget from "../widgets/spacer.js";
|
||||||
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
|
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||||
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
|
import FindWidget from "../widgets/find.js";
|
||||||
import TocWidget from "../widgets/toc.js";
|
import TocWidget from "../widgets/toc.js";
|
||||||
|
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||||
|
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||||
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
|
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||||
|
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
import type { AppContext } from "../components/app_context.js";
|
import type { AppContext } from "../components/app_context.js";
|
||||||
import type { WidgetsByParent } from "../services/bundle.js";
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
import { applyModals } from "./layout_commons.js";
|
||||||
import utils from "../services/utils.js";
|
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx";
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||||
|
import SqlResults from "../widgets/sql_result.js";
|
||||||
|
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||||
|
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||||
|
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 {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@@ -132,19 +129,16 @@ export default class DesktopLayout {
|
|||||||
.child(<CreatePaneButton />)
|
.child(<CreatePaneButton />)
|
||||||
)
|
)
|
||||||
.child(<Ribbon />)
|
.child(<Ribbon />)
|
||||||
|
.child(<SharedInfo />)
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.child(new ContentHeader()
|
.child(new PromotedAttributesWidget())
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
|
||||||
.child(<SharedInfo />)
|
|
||||||
)
|
|
||||||
.child(<PromotedAttributes />)
|
|
||||||
.child(<SqlTableSchemas />)
|
.child(<SqlTableSchemas />)
|
||||||
.child(<NoteDetail />)
|
.child(new NoteDetailWidget())
|
||||||
.child(<NoteList media="screen" />)
|
.child(<NoteList />)
|
||||||
.child(<SearchResult />)
|
.child(<SearchResult />)
|
||||||
.child(<SqlResults />)
|
.child(<SqlResults />)
|
||||||
.child(<ScrollPadding />)
|
.child(<ScrollPadding />)
|
||||||
|
|||||||
@@ -22,8 +22,15 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
|||||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||||
import InfoDialog from "../widgets/dialogs/info.js";
|
import InfoDialog from "../widgets/dialogs/info.js";
|
||||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||||
|
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
||||||
|
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 CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||||
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
|
import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
|
||||||
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
|
|
||||||
export function applyModals(rootContainer: RootContainer) {
|
export function applyModals(rootContainer: RootContainer) {
|
||||||
rootContainer
|
rootContainer
|
||||||
@@ -49,6 +56,16 @@ export function applyModals(rootContainer: RootContainer) {
|
|||||||
.child(<ConfirmDialog />)
|
.child(<ConfirmDialog />)
|
||||||
.child(<PromptDialog />)
|
.child(<PromptDialog />)
|
||||||
.child(<IncorrectCpuArchDialog />)
|
.child(<IncorrectCpuArchDialog />)
|
||||||
.child(<PopupEditorDialog />)
|
.child(new PopupEditorDialog()
|
||||||
|
.child(new FlexContainer("row")
|
||||||
|
.class("title-row")
|
||||||
|
.css("align-items", "center")
|
||||||
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
|
.child(<NoteIconWidget />)
|
||||||
|
.child(<NoteTitleWidget />))
|
||||||
|
.child(<PopupEditorFormattingToolbar />)
|
||||||
|
.child(new PromotedAttributesWidget())
|
||||||
|
.child(new NoteDetailWidget())
|
||||||
|
.child(<NoteList displayOnlyCollections />))
|
||||||
.child(<CallToActionDialog />);
|
.child(<CallToActionDialog />);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,32 @@
|
|||||||
import { applyModals } from "./layout_commons.js";
|
|
||||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
|
||||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
|
||||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
|
||||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
|
||||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import NoteTitleWidget from "../widgets/note_title.js";
|
||||||
import ContentHeader from "../widgets/containers/content_header.js";
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
|
||||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||||
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
|
||||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
|
||||||
import type AppContext from "../components/app_context.js";
|
import type AppContext from "../components/app_context.js";
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
import { applyModals } from "./layout_commons.js";
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||||
|
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||||
|
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";
|
||||||
|
|
||||||
const MOBILE_CSS = `
|
const MOBILE_CSS = `
|
||||||
<style>
|
<style>
|
||||||
span.keyboard-shortcut,
|
|
||||||
kbd {
|
kbd {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -47,8 +40,8 @@ kbd {
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
padding-inline-start: 0.5em;
|
padding-left: 0.5em;
|
||||||
padding-inline-end: 0.5em;
|
padding-right: 0.5em;
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
.quick-search {
|
.quick-search {
|
||||||
@@ -66,7 +59,7 @@ const FANCYTREE_CSS = `
|
|||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
contain: content;
|
contain: content;
|
||||||
padding-inline-start: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fancytree-custom-icon {
|
.fancytree-custom-icon {
|
||||||
@@ -75,7 +68,7 @@ const FANCYTREE_CSS = `
|
|||||||
|
|
||||||
.fancytree-title {
|
.fancytree-title {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
margin-inline-start: 0.6em !important;
|
margin-left: 0.6em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fancytree-node {
|
.fancytree-node {
|
||||||
@@ -88,7 +81,7 @@ const FANCYTREE_CSS = `
|
|||||||
|
|
||||||
span.fancytree-expander {
|
span.fancytree-expander {
|
||||||
width: 24px !important;
|
width: 24px !important;
|
||||||
margin-inline-end: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fancytree-loading span.fancytree-expander {
|
.fancytree-loading span.fancytree-expander {
|
||||||
@@ -108,7 +101,7 @@ span.fancytree-expander {
|
|||||||
.tree-wrapper .scroll-to-active-note-button,
|
.tree-wrapper .scroll-to-active-note-button,
|
||||||
.tree-wrapper .tree-settings-button {
|
.tree-wrapper .tree-settings-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
margin-inline-end: 16px;
|
margin-right: 16px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,8 +126,8 @@ 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")
|
.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")
|
.id("mobile-sidebar-wrapper")
|
||||||
.css("max-height", "100%")
|
.css("max-height", "100%")
|
||||||
.css("padding-inline-start", "0")
|
.css("padding-left", "0")
|
||||||
.css("padding-inline-end", "0")
|
.css("padding-right", "0")
|
||||||
.css("contain", "content")
|
.css("contain", "content")
|
||||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
|
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
|
||||||
)
|
)
|
||||||
@@ -143,7 +136,6 @@ export default class MobileLayout {
|
|||||||
.id("detail-container")
|
.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")
|
.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(
|
.child(
|
||||||
new SplitNoteContainer(() =>
|
|
||||||
new NoteWrapperWidget()
|
new NoteWrapperWidget()
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
@@ -154,27 +146,21 @@ export default class MobileLayout {
|
|||||||
.child(<NoteTitleWidget />)
|
.child(<NoteTitleWidget />)
|
||||||
.child(<MobileDetailMenu />)
|
.child(<MobileDetailMenu />)
|
||||||
)
|
)
|
||||||
|
.child(<SharedInfoWidget />)
|
||||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||||
.child(<PromotedAttributes />)
|
.child(new PromotedAttributesWidget())
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.contentSized()
|
.contentSized()
|
||||||
.child(new ContentHeader()
|
.child(new NoteDetailWidget())
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
.child(<NoteList />)
|
||||||
.child(<SharedInfoWidget />)
|
|
||||||
)
|
|
||||||
.child(<NoteDetail />)
|
|
||||||
.child(<NoteList media="screen" />)
|
|
||||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
|
||||||
.child(<SearchResult />)
|
|
||||||
.child(<FilePropertiesWrapper />)
|
.child(<FilePropertiesWrapper />)
|
||||||
)
|
)
|
||||||
.child(<MobileEditorToolbar />)
|
.child(<MobileEditorToolbar />)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("column")
|
new FlexContainer("column")
|
||||||
.contentSized()
|
.contentSized()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
|
||||||
// @ts-ignore - module = undefined
|
// @ts-ignore - module = undefined
|
||||||
// Required for correct loading of scripts in Electron
|
// Required for correct loading of scripts in Electron
|
||||||
if (typeof module === 'object') {window.module = module; module = undefined;}
|
if (typeof module === 'object') {window.module = module; module = undefined;}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { KeyboardActionNames } from "@triliumnext/commons";
|
|||||||
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import { h, JSX, render } from "preact";
|
import { should } from "vitest";
|
||||||
|
|
||||||
export interface ContextMenuOptions<T> {
|
export interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -15,11 +15,6 @@ export interface ContextMenuOptions<T> {
|
|||||||
onHide?: () => void;
|
onHide?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomMenuItem {
|
|
||||||
kind: "custom",
|
|
||||||
componentFn: () => JSX.Element | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MenuSeparatorItem {
|
export interface MenuSeparatorItem {
|
||||||
kind: "separator";
|
kind: "separator";
|
||||||
}
|
}
|
||||||
@@ -56,7 +51,7 @@ export interface MenuCommandItem<T> {
|
|||||||
columns?: number;
|
columns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T> = MenuCommandItem<T> | CustomMenuItem | MenuSeparatorItem | MenuHeader;
|
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader;
|
||||||
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||||
|
|
||||||
@@ -155,8 +150,8 @@ class ContextMenu {
|
|||||||
this.$widget
|
this.$widget
|
||||||
.css({
|
.css({
|
||||||
display: "block",
|
display: "block",
|
||||||
top,
|
top: top,
|
||||||
left
|
left: left
|
||||||
})
|
})
|
||||||
.addClass("show");
|
.addClass("show");
|
||||||
}
|
}
|
||||||
@@ -165,19 +160,16 @@ class ContextMenu {
|
|||||||
let $group = $parent; // The current group or parent element to which items are being appended
|
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 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
|
let shouldResetGroup = false; // If true, the next item will be the last one from the group
|
||||||
let prevItemKind: string = "";
|
|
||||||
|
|
||||||
for (let index = 0; index < items.length; index++) {
|
for (let index = 0; index < items.length; index++) {
|
||||||
const item = items[index];
|
const item = items[index];
|
||||||
const itemKind = ("kind" in item) ? item.kind : "";
|
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current item is a header, start a new group. This group will contain the
|
// 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.
|
// header and the next item that follows the header.
|
||||||
if (itemKind === "header") {
|
if ("kind" in item && item.kind === "header") {
|
||||||
if (multicolumn && !shouldResetGroup) {
|
if (multicolumn && !shouldResetGroup) {
|
||||||
shouldStartNewGroup = true;
|
shouldStartNewGroup = true;
|
||||||
}
|
}
|
||||||
@@ -203,47 +195,13 @@ class ContextMenu {
|
|||||||
shouldStartNewGroup = false;
|
shouldStartNewGroup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemKind === "separator") {
|
if ("kind" in item && item.kind === "separator") {
|
||||||
if (prevItemKind === "separator") {
|
|
||||||
// Skip consecutive separators
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$group.append($("<div>").addClass("dropdown-divider"));
|
$group.append($("<div>").addClass("dropdown-divider"));
|
||||||
shouldResetGroup = true; // End the group after the next item
|
shouldResetGroup = true; // End the group after the next item
|
||||||
} else if (itemKind === "header") {
|
} else if ("kind" in item && item.kind === "header") {
|
||||||
$group.append($("<h6>").addClass("dropdown-header").text((item as MenuHeader).title));
|
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
|
||||||
shouldResetGroup = true;
|
shouldResetGroup = true;
|
||||||
} else {
|
} else {
|
||||||
if (itemKind === "custom") {
|
|
||||||
// Custom menu item
|
|
||||||
$group.append(this.createCustomMenuItem(item as CustomMenuItem));
|
|
||||||
} else {
|
|
||||||
// Standard menu item
|
|
||||||
$group.append(this.createMenuItem(item as MenuCommandItem<any>));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
prevItemKind = itemKind;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createCustomMenuItem(item: CustomMenuItem) {
|
|
||||||
const element = document.createElement("li");
|
|
||||||
element.classList.add("dropdown-custom-item");
|
|
||||||
element.onclick = () => this.hide();
|
|
||||||
render(h(item.componentFn, {}), element);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createMenuItem(item: MenuCommandItem<any>) {
|
|
||||||
const $icon = $("<span>");
|
const $icon = $("<span>");
|
||||||
|
|
||||||
if ("uiIcon" in item || "checked" in item) {
|
if ("uiIcon" in item || "checked" in item) {
|
||||||
@@ -299,6 +257,8 @@ class ContextMenu {
|
|||||||
// important to use mousedown instead of click since the former does not change focus
|
// important to use mousedown instead of click since the former does not change focus
|
||||||
// (especially important for focused text for spell check)
|
// (especially important for focused text for spell check)
|
||||||
.on("mousedown", (e) => {
|
.on("mousedown", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
if (e.which !== 1) {
|
if (e.which !== 1) {
|
||||||
// only left click triggers menu items
|
// only left click triggers menu items
|
||||||
return false;
|
return false;
|
||||||
@@ -312,11 +272,6 @@ class ContextMenu {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent submenu from failing to expand on mobile
|
|
||||||
if (!("items" in item && item.items)) {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("handler" in item && item.handler) {
|
if ("handler" in item && item.handler) {
|
||||||
item.handler(item, e);
|
item.handler(item, e);
|
||||||
}
|
}
|
||||||
@@ -328,6 +283,16 @@ class ContextMenu {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$item.on("mouseup", (e) => {
|
||||||
|
// Prevent submenu from failing to expand on mobile
|
||||||
|
if (!this.isMobile || !("items" in item && item.items)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
||||||
|
this.hide();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
||||||
$item.addClass("disabled");
|
$item.addClass("disabled");
|
||||||
}
|
}
|
||||||
@@ -346,7 +311,17 @@ class ContextMenu {
|
|||||||
|
|
||||||
$item.append($subMenu);
|
$item.append($subMenu);
|
||||||
}
|
}
|
||||||
return $item;
|
|
||||||
|
$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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async hide() {
|
async hide() {
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { t } from "../services/i18n"
|
|
||||||
import attributes from "../services/attributes"
|
|
||||||
import FNote from "../entities/fnote"
|
|
||||||
|
|
||||||
export function getArchiveMenuItem(note: FNote) {
|
|
||||||
if (!note.isArchived) {
|
|
||||||
return {
|
|
||||||
title: t("board_view.archive-note"),
|
|
||||||
uiIcon: "bx bx-archive",
|
|
||||||
handler: () => attributes.addLabel(note.noteId, "archived")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: t("board_view.unarchive-note"),
|
|
||||||
uiIcon: "bx bx-archive-out",
|
|
||||||
handler: async () => {
|
|
||||||
attributes.removeOwnedLabelByName(note, "archived")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
:root {
|
|
||||||
--note-color-picker-clear-color-cell-background: var(--primary-button-background-color);
|
|
||||||
--note-color-picker-clear-color-cell-color: var(--main-background-color);
|
|
||||||
--note-color-picker-clear-color-cell-selection-outline-color: var(--primary-button-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell {
|
|
||||||
--color-picker-cell-size: 14px;
|
|
||||||
|
|
||||||
width: var(--color-picker-cell-size);
|
|
||||||
height: var(--color-picker-cell-size);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell:not(.selected):hover {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell.disabled-color-cell {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell.selected {
|
|
||||||
outline: 2px solid var(--outline-color, var(--color));
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* RESET COLOR CELL
|
|
||||||
*/
|
|
||||||
|
|
||||||
.note-color-picker .color-cell-reset {
|
|
||||||
--color: var(--note-color-picker-clear-color-cell-background);
|
|
||||||
--outline-color: var(--note-color-picker-clear-color-cell-selection-outline-color);
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .color-cell-reset svg {
|
|
||||||
width: var(--color-picker-cell-size);
|
|
||||||
height: var(--color-picker-cell-size);
|
|
||||||
fill: var(--note-color-picker-clear-color-cell-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* CUSTOM COLOR CELL
|
|
||||||
*/
|
|
||||||
|
|
||||||
.note-color-picker .custom-color-cell::before {
|
|
||||||
position: absolute;
|
|
||||||
content: "\ed35";
|
|
||||||
display: flex;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
font-size: calc(var(--color-picker-cell-size) * 1.3);
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-family: boxicons;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .custom-color-cell {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-color-picker .custom-color-cell.custom-color-cell-empty {
|
|
||||||
background-image: url(./custom-color.png);
|
|
||||||
background-size: cover;
|
|
||||||
--foreground: transparent;
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
import "./NoteColorPicker.css";
|
|
||||||
import { t } from "../../services/i18n";
|
|
||||||
import { useCallback, useEffect, useRef, useState} from "preact/hooks";
|
|
||||||
import {ComponentChildren} from "preact";
|
|
||||||
import attributes from "../../services/attributes";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import Color, { ColorInstance } from "color";
|
|
||||||
import Debouncer from "../../utils/debouncer";
|
|
||||||
import FNote from "../../entities/fnote";
|
|
||||||
import froca from "../../services/froca";
|
|
||||||
import { isMobile } from "../../services/utils";
|
|
||||||
|
|
||||||
const COLOR_PALETTE = [
|
|
||||||
"#e64d4d", "#e6994d", "#e5e64d", "#99e64d", "#4de64d", "#4de699",
|
|
||||||
"#4de5e6", "#4d99e6", "#4d4de6", "#994de6", "#e64db3"
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface NoteColorPickerProps {
|
|
||||||
/** The target Note instance or its ID string. */
|
|
||||||
note: FNote | string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NoteColorPicker(props: NoteColorPickerProps) {
|
|
||||||
if (!props.note) return null;
|
|
||||||
|
|
||||||
const [note, setNote] = useState<FNote | null>(null);
|
|
||||||
const [currentColor, setCurrentColor] = useState<string | null>(null);
|
|
||||||
const [isCustomColor, setIsCustomColor] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const retrieveNote = async (noteId: string) => {
|
|
||||||
const noteInstance = await froca.getNote(noteId, true);
|
|
||||||
if (noteInstance) {
|
|
||||||
setNote(noteInstance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof props.note === "string") {
|
|
||||||
retrieveNote(props.note); // Get the note from the given ID string
|
|
||||||
} else {
|
|
||||||
setNote(props.note);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const colorLabel = note?.getLabel("color")?.value ?? null;
|
|
||||||
if (colorLabel) {
|
|
||||||
let color = tryParseColor(colorLabel);
|
|
||||||
if (color) {
|
|
||||||
setCurrentColor(color.hex().toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [note]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsCustomColor(currentColor !== null && COLOR_PALETTE.indexOf(currentColor) === -1);
|
|
||||||
}, [currentColor])
|
|
||||||
|
|
||||||
const onColorCellClicked = useCallback((color: string | null) => {
|
|
||||||
if (note) {
|
|
||||||
if (color !== null) {
|
|
||||||
attributes.setLabel(note.noteId, "color", color);
|
|
||||||
} else {
|
|
||||||
attributes.removeOwnedLabelByName(note, "color");
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentColor(color);
|
|
||||||
}
|
|
||||||
}, [note, currentColor]);
|
|
||||||
|
|
||||||
return <div className="note-color-picker">
|
|
||||||
|
|
||||||
<ColorCell className="color-cell-reset"
|
|
||||||
tooltip={t("note-color.clear-color")}
|
|
||||||
color={null}
|
|
||||||
isSelected={(currentColor === null)}
|
|
||||||
isDisabled={(note === null)}
|
|
||||||
onSelect={onColorCellClicked}>
|
|
||||||
|
|
||||||
{/* https://pictogrammers.com/library/mdi/icon/close/ */}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
|
||||||
</svg>
|
|
||||||
</ColorCell>
|
|
||||||
|
|
||||||
|
|
||||||
{COLOR_PALETTE.map((color) => (
|
|
||||||
<ColorCell key={color}
|
|
||||||
tooltip={t("note-color.set-color")}
|
|
||||||
color={color}
|
|
||||||
isSelected={(color === currentColor)}
|
|
||||||
isDisabled={(note === null)}
|
|
||||||
onSelect={onColorCellClicked} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
<CustomColorCell tooltip={t("note-color.set-custom-color")}
|
|
||||||
color={currentColor}
|
|
||||||
isSelected={isCustomColor}
|
|
||||||
isDisabled={(note === null)}
|
|
||||||
onSelect={onColorCellClicked} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColorCellProps {
|
|
||||||
children?: ComponentChildren,
|
|
||||||
className?: string,
|
|
||||||
tooltip?: string,
|
|
||||||
color: string | null,
|
|
||||||
isSelected: boolean,
|
|
||||||
isDisabled?: boolean,
|
|
||||||
onSelect?: (color: string | null) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColorCell(props: ColorCellProps) {
|
|
||||||
return <div className={clsx(props.className, {
|
|
||||||
"color-cell": true,
|
|
||||||
"selected": props.isSelected,
|
|
||||||
"disabled-color-cell": props.isDisabled
|
|
||||||
})}
|
|
||||||
style={`${(props.color !== null) ? `--color: ${props.color}` : ""}`}
|
|
||||||
title={props.tooltip}
|
|
||||||
onClick={() => props.onSelect?.(props.color)}>
|
|
||||||
{props.children}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CustomColorCell(props: ColorCellProps) {
|
|
||||||
const [pickedColor, setPickedColor] = useState<string | null>(null);
|
|
||||||
const colorInput = useRef<HTMLInputElement>(null);
|
|
||||||
const colorInputDebouncer = useRef<Debouncer<string | null> | null>(null);
|
|
||||||
const callbackRef = useRef(props.onSelect);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
colorInputDebouncer.current = new Debouncer(250, (color) => {
|
|
||||||
callbackRef.current?.(color);
|
|
||||||
setPickedColor(color);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
colorInputDebouncer.current?.destroy();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.isSelected && pickedColor === null) {
|
|
||||||
setPickedColor(props.color);
|
|
||||||
}
|
|
||||||
}, [props.isSelected])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
callbackRef.current = props.onSelect;
|
|
||||||
}, [props.onSelect]);
|
|
||||||
|
|
||||||
const onSelect = useCallback(() => {
|
|
||||||
if (pickedColor !== null) {
|
|
||||||
callbackRef.current?.(pickedColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
colorInput.current?.click();
|
|
||||||
}, [pickedColor]);
|
|
||||||
|
|
||||||
return <div style={`--foreground: ${getForegroundColor(props.color)};`}
|
|
||||||
onClick={isMobile() ? (e) => {
|
|
||||||
// The color picker dropdown will close on some browser if the parent context menu is
|
|
||||||
// dismissed, so stop the click propagation to prevent dismissing the menu.
|
|
||||||
e.stopPropagation();
|
|
||||||
} : undefined}>
|
|
||||||
<ColorCell {...props}
|
|
||||||
color={pickedColor}
|
|
||||||
className={clsx("custom-color-cell", {
|
|
||||||
"custom-color-cell-empty": (pickedColor === null)
|
|
||||||
})}
|
|
||||||
onSelect={onSelect}>
|
|
||||||
|
|
||||||
<input ref={colorInput}
|
|
||||||
type="color"
|
|
||||||
value={pickedColor ?? props.color ?? "#40bfbf"}
|
|
||||||
onChange={() => {colorInputDebouncer.current?.updateValue(colorInput.current?.value ?? null)}}
|
|
||||||
style="width: 0; height: 0; opacity: 0" />
|
|
||||||
</ColorCell>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function getForegroundColor(backgroundColor: string | null) {
|
|
||||||
if (backgroundColor === null) return "inherit";
|
|
||||||
|
|
||||||
const colorHsl = tryParseColor(backgroundColor)?.hsl();
|
|
||||||
if (colorHsl) {
|
|
||||||
let l = colorHsl.lightness();
|
|
||||||
return colorHsl.saturationl(0).lightness(l >= 50 ? 0 : 100).hex();
|
|
||||||
} else {
|
|
||||||
return "inherit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryParseColor(colorStr: string): ColorInstance | null {
|
|
||||||
try {
|
|
||||||
return new Color(colorStr);
|
|
||||||
} catch(ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@ import zoomService from "../components/zoom.js";
|
|||||||
import contextMenu, { type MenuItem } from "./context_menu.js";
|
import contextMenu, { type MenuItem } from "./context_menu.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import type { BrowserWindow } from "electron";
|
import type { BrowserWindow } from "electron";
|
||||||
import type { CommandNames, AppContext } from "../components/app_context.js";
|
import type { CommandNames } from "../components/app_context.js";
|
||||||
|
|
||||||
function setupContextMenu() {
|
function setupContextMenu() {
|
||||||
const electron = utils.dynamicRequire("electron");
|
const electron = utils.dynamicRequire("electron");
|
||||||
@@ -13,8 +13,6 @@ function setupContextMenu() {
|
|||||||
// FIXME: Remove typecast once Electron is properly integrated.
|
// FIXME: Remove typecast once Electron is properly integrated.
|
||||||
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
||||||
|
|
||||||
let appContext: AppContext;
|
|
||||||
|
|
||||||
webContents.on("context-menu", (event, params) => {
|
webContents.on("context-menu", (event, params) => {
|
||||||
const { editFlags } = params;
|
const { editFlags } = params;
|
||||||
const hasText = params.selectionText.trim().length > 0;
|
const hasText = params.selectionText.trim().length > 0;
|
||||||
@@ -121,20 +119,6 @@ function setupContextMenu() {
|
|||||||
uiIcon: "bx bx-search-alt",
|
uiIcon: "bx bx-search-alt",
|
||||||
handler: () => electron.shell.openExternal(searchUrl)
|
handler: () => electron.shell.openExternal(searchUrl)
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push({
|
|
||||||
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
|
|
||||||
uiIcon: "bx bx-search",
|
|
||||||
handler: async () => {
|
|
||||||
if (!appContext) {
|
|
||||||
appContext = (await import("../components/app_context.js")).default;
|
|
||||||
}
|
|
||||||
|
|
||||||
await appContext.triggerCommand("searchNotes", {
|
|
||||||
searchString: params.selectionText
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
|||||||
@@ -2,32 +2,26 @@ import { t } from "../services/i18n.js";
|
|||||||
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
||||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
import type { ViewScope } from "../services/link.js";
|
import type { ViewScope } from "../services/link.js";
|
||||||
import utils, { isMobile } from "../services/utils.js";
|
|
||||||
import { getClosestNtxId } from "../widgets/widget_utils.js";
|
|
||||||
import type { LeafletMouseEvent } from "leaflet";
|
|
||||||
|
|
||||||
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
||||||
contextMenu.show({
|
contextMenu.show({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: getItems(e),
|
items: getItems(),
|
||||||
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, e, notePath, viewScope, hoistedNoteId)
|
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
|
function getItems(): MenuItem<CommandNames>[] {
|
||||||
const ntxId = getNtxId(e);
|
|
||||||
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
||||||
{ title: !isMobileSplitOpen ? t("link_context_menu.open_note_in_new_split") : t("link_context_menu.open_note_in_other_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
||||||
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||||
if (!hoistedNoteId) {
|
if (!hoistedNoteId) {
|
||||||
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
||||||
}
|
}
|
||||||
@@ -35,8 +29,15 @@ function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEv
|
|||||||
if (command === "openNoteInNewTab") {
|
if (command === "openNoteInNewTab") {
|
||||||
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
||||||
} else if (command === "openNoteInNewSplit") {
|
} else if (command === "openNoteInNewSplit") {
|
||||||
const ntxId = getNtxId(e);
|
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||||
if (!ntxId) return;
|
|
||||||
|
if (!subContexts) {
|
||||||
|
logError("subContexts is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ntxId } = subContexts[subContexts.length - 1];
|
||||||
|
|
||||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||||
} else if (command === "openNoteInNewWindow") {
|
} else if (command === "openNoteInNewWindow") {
|
||||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||||
@@ -45,18 +46,6 @@ function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNtxId(e: ContextMenuEvent | LeafletMouseEvent) {
|
|
||||||
if (utils.isDesktop()) {
|
|
||||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
|
||||||
if (!subContexts) return null;
|
|
||||||
return subContexts[subContexts.length - 1].ntxId;
|
|
||||||
} else if (e.target instanceof HTMLElement) {
|
|
||||||
return getClosestNtxId(e.target);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getItems,
|
getItems,
|
||||||
handleLinkContextMenuItem,
|
handleLinkContextMenuItem,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
|
||||||
import treeService from "../services/tree.js";
|
import treeService from "../services/tree.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import clipboard from "../services/clipboard.js";
|
import clipboard from "../services/clipboard.js";
|
||||||
@@ -138,15 +137,9 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
command: "editBranchPrefix",
|
command: "editBranchPrefix",
|
||||||
keyboardShortcut: "editBranchPrefix",
|
keyboardShortcut: "editBranchPrefix",
|
||||||
uiIcon: "bx bx-rename",
|
uiIcon: "bx bx-rename",
|
||||||
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
||||||
},
|
|
||||||
{
|
|
||||||
title:
|
|
||||||
t("tree-context-menu.convert-to-attachment"),
|
|
||||||
command: "convertNoteToAttachment",
|
|
||||||
uiIcon: "bx bx-paperclip",
|
|
||||||
enabled: isNotRoot && !isHoisted && notOptionsOrHelp && selectedNotes.some(note => note.isEligibleForConversionToAttachment())
|
|
||||||
},
|
},
|
||||||
|
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||||
|
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
|
||||||
@@ -248,15 +241,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
||||||
},
|
},
|
||||||
|
|
||||||
{ kind: "separator"},
|
|
||||||
|
|
||||||
(notOptionsOrHelp && selectedNotes.length === 1) ? {
|
|
||||||
kind: "custom",
|
|
||||||
componentFn: () => {
|
|
||||||
return NoteColorPicker({note});
|
|
||||||
}
|
|
||||||
} : null,
|
|
||||||
|
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
|
|
||||||
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import appContext from "./components/app_context.js";
|
import appContext from "./components/app_context.js";
|
||||||
import noteAutocompleteService from "./services/note_autocomplete.js";
|
import noteAutocompleteService from "./services/note_autocomplete.js";
|
||||||
import glob from "./services/glob.js";
|
import glob from "./services/glob.js";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "boxicons/css/boxicons.min.css";
|
import "boxicons/css/boxicons.min.css";
|
||||||
import "autocomplete.js/index_jquery.js";
|
import "autocomplete.js/index_jquery.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
@import "boxicons/css/boxicons.min.css";
|
|
||||||
|
|
||||||
: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,151 +0,0 @@
|
|||||||
import FNote from "./entities/fnote";
|
|
||||||
import { render } from "preact";
|
|
||||||
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
|
|
||||||
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
|
||||||
import content_renderer from "./services/content_renderer";
|
|
||||||
import { dynamicRequire, isElectron } from "./services/utils";
|
|
||||||
import { applyInlineMermaid } from "./services/content_renderer_text";
|
|
||||||
|
|
||||||
interface RendererProps {
|
|
||||||
note: FNote;
|
|
||||||
onReady: () => void;
|
|
||||||
onProgressChanged?: (progress: number) => 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 onProgressChanged = useCallback((progress: number) => {
|
|
||||||
if (isElectron()) {
|
|
||||||
const { ipcRenderer } = dynamicRequire('electron');
|
|
||||||
ipcRenderer.send("print-progress", progress);
|
|
||||||
} else {
|
|
||||||
window.dispatchEvent(new CustomEvent("note-load-progress", { detail: { progress } }));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
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, onProgressChanged };
|
|
||||||
|
|
||||||
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 });
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize mermaid.
|
|
||||||
if (note.type === "text") {
|
|
||||||
await applyInlineMermaid(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check custom CSS.
|
|
||||||
await loadCustomCss(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
load().then(() => requestAnimationFrame(onReady))
|
|
||||||
}, [ note ]);
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<h1>{note.title}</h1>
|
|
||||||
<main ref={containerRef} />
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps) {
|
|
||||||
const viewType = useNoteViewType(note);
|
|
||||||
return <CustomNoteList
|
|
||||||
viewType={viewType}
|
|
||||||
isEnabled
|
|
||||||
note={note}
|
|
||||||
notePath={note.getBestNotePath().join("/")}
|
|
||||||
ntxId="print"
|
|
||||||
highlightedTokens={null}
|
|
||||||
media="print"
|
|
||||||
onReady={async () => {
|
|
||||||
await loadCustomCss(note);
|
|
||||||
onReady();
|
|
||||||
}}
|
|
||||||
onProgressChanged={onProgressChanged}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Error404({ noteId }: { noteId: string }) {
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
<p>The note you are trying to print could not be found.</p>
|
|
||||||
<small>{noteId}</small>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCustomCss(note: FNote) {
|
|
||||||
const printCssNotes = await note.getRelationTargets("printCss");
|
|
||||||
let loadPromises: JQueryPromise<void>[] = [];
|
|
||||||
|
|
||||||
for (const printCssNote of printCssNotes) {
|
|
||||||
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
|
|
||||||
|
|
||||||
const linkEl = document.createElement("link");
|
|
||||||
linkEl.href = `/api/notes/${printCssNote.noteId}/download`;
|
|
||||||
linkEl.rel = "stylesheet";
|
|
||||||
|
|
||||||
const promise = $.Deferred();
|
|
||||||
loadPromises.push(promise.promise());
|
|
||||||
linkEl.onload = () => promise.resolve();
|
|
||||||
|
|
||||||
document.head.appendChild(linkEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.allSettled(loadPromises);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,15 +1,5 @@
|
|||||||
import $ from "jquery";
|
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).$ = $;
|
||||||
(window as any).jQuery = $;
|
(window as any).jQuery = $;
|
||||||
await loadBootstrap();
|
|
||||||
|
|
||||||
$("body").show();
|
$("body").show();
|
||||||
|
|||||||
@@ -90,8 +90,7 @@ const HIDDEN_ATTRIBUTES = [
|
|||||||
"viewType",
|
"viewType",
|
||||||
"geolocation",
|
"geolocation",
|
||||||
"docName",
|
"docName",
|
||||||
"webViewSrc",
|
"webViewSrc"
|
||||||
"archived"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
async function renderNormalAttributes(note: FNote) {
|
async function renderNormalAttributes(note: FNote) {
|
||||||
|
|||||||
@@ -22,15 +22,6 @@ export async function setLabel(noteId: string, name: string, value: string = "",
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
|
|
||||||
await server.put(`notes/${noteId}/set-attribute`, {
|
|
||||||
type: "relation",
|
|
||||||
name: name,
|
|
||||||
value: value,
|
|
||||||
isInheritable
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeAttributeById(noteId: string, attributeId: string) {
|
async function removeAttributeById(noteId: string, attributeId: string) {
|
||||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||||
}
|
}
|
||||||
@@ -60,23 +51,6 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a relation identified by its name from the given note, if it exists. Note that the relation must be owned, i.e.
|
|
||||||
* it will not remove inherited attributes.
|
|
||||||
*
|
|
||||||
* @param note the note from which to remove the relation.
|
|
||||||
* @param relationName the name of the relation to remove.
|
|
||||||
* @returns `true` if an attribute was identified and removed, `false` otherwise.
|
|
||||||
*/
|
|
||||||
function removeOwnedRelationByName(note: FNote, relationName: string) {
|
|
||||||
const relation = note.getOwnedRelation(relationName);
|
|
||||||
if (relation) {
|
|
||||||
removeAttributeById(note.noteId, relation.attributeId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
||||||
* For an attribute with an empty value, pass an empty string instead.
|
* For an attribute with an empty value, pass an empty string instead.
|
||||||
@@ -126,7 +100,9 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrRow.isInheritable) {
|
// TODO: This doesn't seem right.
|
||||||
|
//@ts-ignore
|
||||||
|
if (this.isInheritable) {
|
||||||
for (const owningNote of owningNotes) {
|
for (const owningNote of owningNotes) {
|
||||||
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -140,10 +116,8 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
|||||||
export default {
|
export default {
|
||||||
addLabel,
|
addLabel,
|
||||||
setLabel,
|
setLabel,
|
||||||
setRelation,
|
|
||||||
setAttribute,
|
setAttribute,
|
||||||
removeAttributeById,
|
removeAttributeById,
|
||||||
removeOwnedLabelByName,
|
removeOwnedLabelByName,
|
||||||
removeOwnedRelationByName,
|
|
||||||
isAffecting
|
isAffecting
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ async function moveNodeUpInHierarchy(node: Fancytree.FancytreeNode) {
|
|||||||
toastService.showError(resp.message);
|
toastService.showError(resp.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hoistedNoteService.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
|
||||||
|
node.getParent().folder = false;
|
||||||
|
node.getParent().renderTitle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterSearchBranches(branchIds: string[]) {
|
function filterSearchBranches(branchIds: string[]) {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { Bundle, executeBundle } from "./bundle";
|
|
||||||
import { buildNote } from "../test/easy-froca";
|
|
||||||
|
|
||||||
describe("Script bundle", () => {
|
|
||||||
it("dayjs is available", async () => {
|
|
||||||
const script = /* js */`return api.dayjs().format("YYYY-MM-DD");`;
|
|
||||||
const bundle = getBundle(script);
|
|
||||||
const result = await executeBundle(bundle, null, $());
|
|
||||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dayjs is-same-or-before plugin exists", async () => {
|
|
||||||
const script = /* js */`return api.dayjs("2023-10-01").isSameOrBefore(api.dayjs("2023-10-02"));`;
|
|
||||||
const bundle = getBundle(script);
|
|
||||||
const result = await executeBundle(bundle, null, $());
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function getBundle(script: string) {
|
|
||||||
const id = buildNote({
|
|
||||||
title: "Script note"
|
|
||||||
}).noteId;
|
|
||||||
const bundle: Bundle = {
|
|
||||||
script: [
|
|
||||||
'',
|
|
||||||
`apiContext.modules['${id}'] = { exports: {} };`,
|
|
||||||
`return await ((async function(exports, module, require, api) {`,
|
|
||||||
`try {`,
|
|
||||||
`${script}`,
|
|
||||||
`;`,
|
|
||||||
`} catch (e) { throw new Error(\"Load of script note \\\"Client\\\" (${id}) failed with: \" + e.message); }`,
|
|
||||||
`for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];`,
|
|
||||||
`return module.exports;`,
|
|
||||||
`}).call({}, {}, apiContext.modules['${id}'], apiContext.require([]), apiContext.apis['${id}']));`,
|
|
||||||
''
|
|
||||||
].join('\n'),
|
|
||||||
html: "",
|
|
||||||
noteId: id,
|
|
||||||
allNoteIds: [ id ]
|
|
||||||
};
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ async function getAndExecuteBundle(noteId: string, originEntity = null, script =
|
|||||||
return await executeBundle(bundle, originEntity);
|
return await executeBundle(bundle, originEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
||||||
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,31 +2,32 @@ import renderService from "./render.js";
|
|||||||
import protectedSessionService from "./protected_session.js";
|
import protectedSessionService from "./protected_session.js";
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
import openService from "./open.js";
|
import openService from "./open.js";
|
||||||
|
import froca from "./froca.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
|
import linkService from "./link.js";
|
||||||
|
import treeService from "./tree.js";
|
||||||
import FNote from "../entities/fnote.js";
|
import FNote from "../entities/fnote.js";
|
||||||
import FAttachment from "../entities/fattachment.js";
|
import FAttachment from "../entities/fattachment.js";
|
||||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||||
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
|
import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js";
|
||||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||||
import renderDoc from "./doc_renderer.js";
|
import renderDoc from "./doc_renderer.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||||
|
import { renderMathInElement } from "./math.js";
|
||||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||||
import renderText from "./content_renderer_text.js";
|
|
||||||
|
|
||||||
let idCounter = 1;
|
let idCounter = 1;
|
||||||
|
|
||||||
export interface RenderOptions {
|
interface Options {
|
||||||
tooltip?: boolean;
|
tooltip?: boolean;
|
||||||
trim?: boolean;
|
trim?: boolean;
|
||||||
imageHasZoom?: 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"]);
|
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||||
|
|
||||||
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: RenderOptions = {}) {
|
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
||||||
|
|
||||||
options = Object.assign(
|
options = Object.assign(
|
||||||
{
|
{
|
||||||
@@ -41,7 +42,7 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
|||||||
const $renderedContent = $('<div class="rendered-content">');
|
const $renderedContent = $('<div class="rendered-content">');
|
||||||
|
|
||||||
if (type === "text" || type === "book") {
|
if (type === "text" || type === "book") {
|
||||||
await renderText(entity, $renderedContent, options);
|
await renderText(entity, $renderedContent);
|
||||||
} else if (type === "code") {
|
} else if (type === "code") {
|
||||||
await renderCode(entity, $renderedContent);
|
await renderCode(entity, $renderedContent);
|
||||||
} else if (["image", "canvas", "mindMap"].includes(type)) {
|
} else if (["image", "canvas", "mindMap"].includes(type)) {
|
||||||
@@ -113,6 +114,32 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||||
|
// entity must be FNote
|
||||||
|
const blob = await note.getBlob();
|
||||||
|
|
||||||
|
if (blob && !utils.isHtmlEmpty(blob.content)) {
|
||||||
|
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
||||||
|
|
||||||
|
if ($renderedContent.find("span.math-tex").length > 0) {
|
||||||
|
renderMathInElement($renderedContent[0], { trust: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr("href") || "");
|
||||||
|
const referenceLinks = $renderedContent.find("a.reference-link");
|
||||||
|
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
|
||||||
|
await froca.getNotes(noteIdsToPrefetch);
|
||||||
|
|
||||||
|
for (const el of referenceLinks) {
|
||||||
|
await linkService.loadReferenceLinkTitle($(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
await formatCodeBlocks($renderedContent);
|
||||||
|
} else if (note instanceof FNote) {
|
||||||
|
await renderChildrenList($renderedContent, note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
||||||
*/
|
*/
|
||||||
@@ -134,7 +161,7 @@ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
|||||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
|
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
|
||||||
const encodedTitle = encodeURIComponent(entity.title);
|
const encodedTitle = encodeURIComponent(entity.title);
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
@@ -276,6 +303,40 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {jQuery} $renderedContent
|
||||||
|
* @param {FNote} note
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
||||||
|
let childNoteIds = note.getChildNoteIds();
|
||||||
|
|
||||||
|
if (!childNoteIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$renderedContent.css("padding", "10px");
|
||||||
|
$renderedContent.addClass("text-with-ellipsis");
|
||||||
|
|
||||||
|
if (childNoteIds.length > 10) {
|
||||||
|
childNoteIds = childNoteIds.slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// just load the first 10 child notes
|
||||||
|
const childNotes = await froca.getNotes(childNoteIds);
|
||||||
|
|
||||||
|
for (const childNote of childNotes) {
|
||||||
|
$renderedContent.append(
|
||||||
|
await linkService.createLink(`${note.noteId}/${childNote.noteId}`, {
|
||||||
|
showTooltip: false,
|
||||||
|
showNoteIcon: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$renderedContent.append("<br>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getRenderingType(entity: FNote | FAttachment) {
|
function getRenderingType(entity: FNote | FAttachment) {
|
||||||
let type: string = "";
|
let type: string = "";
|
||||||
if ("type" in entity) {
|
if ("type" in entity) {
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import { formatCodeBlocks } from "./syntax_highlight.js";
|
|
||||||
import { getMermaidConfig } from "./mermaid.js";
|
|
||||||
import { renderMathInElement } from "./math.js";
|
|
||||||
import FNote from "../entities/fnote.js";
|
|
||||||
import FAttachment from "../entities/fattachment.js";
|
|
||||||
import tree from "./tree.js";
|
|
||||||
import froca from "./froca.js";
|
|
||||||
import link from "./link.js";
|
|
||||||
import { isHtmlEmpty } from "./utils.js";
|
|
||||||
import { default as content_renderer, type RenderOptions } from "./content_renderer.js";
|
|
||||||
|
|
||||||
export default async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
|
|
||||||
// entity must be FNote
|
|
||||||
const blob = await note.getBlob();
|
|
||||||
|
|
||||||
if (blob && !isHtmlEmpty(blob.content)) {
|
|
||||||
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
|
||||||
await renderIncludedNotes($renderedContent[0]);
|
|
||||||
|
|
||||||
if ($renderedContent.find("span.math-tex").length > 0) {
|
|
||||||
renderMathInElement($renderedContent[0], { trust: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNoteIdFromLink = (el: HTMLElement) => tree.getNoteIdFromUrl($(el).attr("href") || "");
|
|
||||||
const referenceLinks = $renderedContent.find("a.reference-link");
|
|
||||||
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
|
|
||||||
await froca.getNotes(noteIdsToPrefetch);
|
|
||||||
|
|
||||||
for (const el of referenceLinks) {
|
|
||||||
await link.loadReferenceLinkTitle($(el));
|
|
||||||
}
|
|
||||||
|
|
||||||
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
|
|
||||||
await formatCodeBlocks($renderedContent);
|
|
||||||
} else if (note instanceof FNote && !options.noChildrenList) {
|
|
||||||
await renderChildrenList($renderedContent, note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderIncludedNotes(contentEl: HTMLElement) {
|
|
||||||
// TODO: Consider duplicating with server's share/content_renderer.ts.
|
|
||||||
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
|
|
||||||
|
|
||||||
// Gather the list of items to load.
|
|
||||||
const noteIds: string[] = [];
|
|
||||||
for (const includeNoteEl of includeNoteEls) {
|
|
||||||
const noteId = includeNoteEl.getAttribute("data-note-id");
|
|
||||||
if (noteId) {
|
|
||||||
noteIds.push(noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the required notes.
|
|
||||||
await froca.getNotes(noteIds);
|
|
||||||
|
|
||||||
// Render and integrate the notes.
|
|
||||||
for (const includeNoteEl of includeNoteEls) {
|
|
||||||
const noteId = includeNoteEl.getAttribute("data-note-id");
|
|
||||||
if (!noteId) continue;
|
|
||||||
|
|
||||||
const note = froca.getNoteFromCache(noteId);
|
|
||||||
if (!note) {
|
|
||||||
console.warn(`Unable to include ${noteId} because it could not be found.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent;
|
|
||||||
includeNoteEl.replaceChildren(...renderedContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Rewrite the code block from <pre><code> to <div> in order not to apply a codeblock style to it. */
|
|
||||||
export async function rewriteMermaidDiagramsInContainer(container: HTMLDivElement) {
|
|
||||||
const mermaidBlocks = container.querySelectorAll('pre:has(code[class="language-mermaid"])');
|
|
||||||
if (!mermaidBlocks.length) return;
|
|
||||||
const nodes: HTMLElement[] = [];
|
|
||||||
|
|
||||||
for (const mermaidBlock of mermaidBlocks) {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.classList.add("mermaid-diagram");
|
|
||||||
div.innerHTML = mermaidBlock.querySelector("code")?.innerHTML ?? "";
|
|
||||||
mermaidBlock.replaceWith(div);
|
|
||||||
nodes.push(div);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyInlineMermaid(container: HTMLDivElement) {
|
|
||||||
// Initialize mermaid
|
|
||||||
const mermaid = (await import("mermaid")).default;
|
|
||||||
mermaid.initialize(getMermaidConfig());
|
|
||||||
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
|
|
||||||
try {
|
|
||||||
await mermaid.run({ nodes });
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
|
||||||
let childNoteIds = note.getChildNoteIds();
|
|
||||||
|
|
||||||
if (!childNoteIds.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$renderedContent.css("padding", "10px");
|
|
||||||
$renderedContent.addClass("text-with-ellipsis");
|
|
||||||
|
|
||||||
if (childNoteIds.length > 10) {
|
|
||||||
childNoteIds = childNoteIds.slice(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// just load the first 10 child notes
|
|
||||||
const childNotes = await froca.getNotes(childNoteIds);
|
|
||||||
|
|
||||||
for (const childNote of childNotes) {
|
|
||||||
$renderedContent.append(
|
|
||||||
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
|
|
||||||
showTooltip: false,
|
|
||||||
showNoteIcon: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
$renderedContent.append("<br>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +1,26 @@
|
|||||||
import clsx from "clsx";
|
|
||||||
import {readCssVar} from "../utils/css-var";
|
|
||||||
import Color, { ColorInstance } from "color";
|
|
||||||
|
|
||||||
const registeredClasses = new Set<string>();
|
const registeredClasses = new Set<string>();
|
||||||
const colorsWithHue = 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(
|
const normalizedColorName = color.replace(/[^a-z0-9]/gi, "");
|
||||||
document.documentElement,
|
|
||||||
"tree-item-light-theme-max-color-lightness"
|
|
||||||
).asNumber(70);
|
|
||||||
|
|
||||||
const darkThemeColorMinLightness = readCssVar(
|
if (!normalizedColorName.trim()) {
|
||||||
document.documentElement,
|
return "";
|
||||||
"tree-item-dark-theme-min-color-lightness"
|
}
|
||||||
).asNumber(50);
|
|
||||||
|
|
||||||
function createClassForColor(colorString: string | null) {
|
const className = `color-${normalizedColorName}`;
|
||||||
if (!colorString?.trim()) return "";
|
|
||||||
|
|
||||||
const color = parseColor(colorString);
|
|
||||||
if (!color) return "";
|
|
||||||
|
|
||||||
const className = `color-${color.hex().substring(1)}`;
|
|
||||||
|
|
||||||
if (!registeredClasses.has(className)) {
|
if (!registeredClasses.has(className)) {
|
||||||
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
|
// make the active fancytree selector more specific than the normal color setting
|
||||||
darkThemeColorMinLightness!);
|
$("head").append(`<style>.${className}, span.fancytree-active.${className} { color: ${color} !important; }</style>`);
|
||||||
const hue = getHue(color);
|
|
||||||
|
|
||||||
$("head").append(`<style>
|
|
||||||
.${className}, span.fancytree-active.${className} {
|
|
||||||
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
|
|
||||||
--dark-theme-custom-color: ${adjustedColor.darkThemeColor};
|
|
||||||
--custom-color-hue: ${hue ?? 'unset'};
|
|
||||||
}
|
|
||||||
</style>`);
|
|
||||||
|
|
||||||
registeredClasses.add(className);
|
registeredClasses.add(className);
|
||||||
if (hue !== undefined) {
|
|
||||||
colorsWithHue.add(className);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
|
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 function getReadableTextColor(bgColor: string) {
|
|
||||||
const colorInstance = Color(bgColor);
|
|
||||||
return colorInstance.isLight() ? "#000" : "#fff";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { dayjs } from "@triliumnext/commons";
|
import dayjs from "dayjs";
|
||||||
import type { FNoteRow } from "../entities/fnote.js";
|
import type { FNoteRow } from "../entities/fnote.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* @param whether to execute at the beginning (`false`)
|
* @param whether to execute at the beginning (`false`)
|
||||||
* @api public
|
* @api public
|
||||||
*/
|
*/
|
||||||
function debounce<T>(func: (...args: any[]) => T, waitMs: number, immediate: boolean = false) {
|
function debounce<T>(func: (...args: unknown[]) => T, waitMs: number, immediate: boolean = false) {
|
||||||
let timeout: any; // TODO: fix once we split client and server.
|
let timeout: any; // TODO: fix once we split client and server.
|
||||||
let args: unknown[] | null;
|
let args: unknown[] | null;
|
||||||
let context: unknown;
|
let context: unknown;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_helper.js";
|
|
||||||
import { getCurrentLanguage } from "./i18n.js";
|
import { getCurrentLanguage } from "./i18n.js";
|
||||||
import { formatCodeBlocks } from "./syntax_highlight.js";
|
import { formatCodeBlocks } from "./syntax_highlight.js";
|
||||||
|
|
||||||
@@ -11,18 +10,18 @@ export default function renderDoc(note: FNote) {
|
|||||||
if (docName) {
|
if (docName) {
|
||||||
// find doc based on language
|
// find doc based on language
|
||||||
const url = getUrl(docName, getCurrentLanguage());
|
const url = getUrl(docName, getCurrentLanguage());
|
||||||
$content.load(url, async (response, status) => {
|
$content.load(url, (response, status) => {
|
||||||
// fallback to english doc if no translation available
|
// fallback to english doc if no translation available
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
const fallbackUrl = getUrl(docName, "en");
|
const fallbackUrl = getUrl(docName, "en");
|
||||||
$content.load(fallbackUrl, async () => {
|
$content.load(fallbackUrl, () => {
|
||||||
await processContent(fallbackUrl, $content)
|
processContent(fallbackUrl, $content)
|
||||||
resolve($content);
|
resolve($content);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await processContent(url, $content);
|
processContent(url, $content);
|
||||||
resolve($content);
|
resolve($content);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -33,7 +32,7 @@ export default function renderDoc(note: FNote) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processContent(url: string, $content: JQuery<HTMLElement>) {
|
function processContent(url: string, $content: JQuery<HTMLElement>) {
|
||||||
const dir = url.substring(0, url.lastIndexOf("/"));
|
const dir = url.substring(0, url.lastIndexOf("/"));
|
||||||
|
|
||||||
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
|
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
|
||||||
@@ -43,9 +42,6 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
formatCodeBlocks($content);
|
formatCodeBlocks($content);
|
||||||
|
|
||||||
// Apply reference links.
|
|
||||||
await applyReferenceLinks($content[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrl(docNameValue: string, language: string) {
|
function getUrl(docNameValue: string, language: string) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface Froca {
|
|||||||
|
|
||||||
getBlob(entityType: string, entityId: string): Promise<FBlob | null>;
|
getBlob(entityType: string, entityId: string): Promise<FBlob | null>;
|
||||||
getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>;
|
getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>;
|
||||||
getNoteFromCache(noteId: string): FNote | undefined;
|
getNoteFromCache(noteId: string): FNote;
|
||||||
getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[];
|
getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[];
|
||||||
getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise<FNote[]>;
|
getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise<FNote[]>;
|
||||||
|
|
||||||
|
|||||||
@@ -40,23 +40,20 @@ class FrocaImpl implements Froca {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initializedPromise = this.loadInitialTree();
|
this.initializedPromise = this.loadInitialTree();
|
||||||
this.#clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadInitialTree() {
|
async loadInitialTree() {
|
||||||
const resp = await server.get<SubtreeResponse>("tree");
|
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
|
// 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.notes = {};
|
||||||
this.branches = {};
|
this.branches = {};
|
||||||
this.attributes = {};
|
this.attributes = {};
|
||||||
this.attachments = {};
|
this.attachments = {};
|
||||||
this.blobPromises = {};
|
this.blobPromises = {};
|
||||||
|
|
||||||
|
this.addResp(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSubTree(subTreeNoteId: string) {
|
async loadSubTree(subTreeNoteId: string) {
|
||||||
@@ -288,7 +285,7 @@ class FrocaImpl implements Froca {
|
|||||||
return (await this.getNotes([noteId], silentNotFoundError))[0];
|
return (await this.getNotes([noteId], silentNotFoundError))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteFromCache(noteId: string): FNote | undefined {
|
getNoteFromCache(noteId: string) {
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
throw new Error("Empty noteId");
|
throw new Error("Empty noteId");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ import RightPanelWidget from "../widgets/right_panel_widget.js";
|
|||||||
import ws from "./ws.js";
|
import ws from "./ws.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
|
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
|
||||||
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
import BasicWidget from "../widgets/basic_widget.js";
|
||||||
import SpacedUpdate from "./spaced_update.js";
|
import SpacedUpdate from "./spaced_update.js";
|
||||||
import shortcutService from "./shortcuts.js";
|
import shortcutService from "./shortcuts.js";
|
||||||
import dialogService from "./dialog.js";
|
import dialogService from "./dialog.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import { dayjs } from "@triliumnext/commons";
|
import dayjs from "dayjs";
|
||||||
import type NoteContext from "../components/note_context.js";
|
import type NoteContext from "../components/note_context.js";
|
||||||
|
import type NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import type Component from "../components/component.js";
|
import type Component from "../components/component.js";
|
||||||
import { formatLogMessage } from "@triliumnext/commons";
|
import { formatLogMessage } from "@triliumnext/commons";
|
||||||
|
|
||||||
@@ -316,7 +317,7 @@ export interface Api {
|
|||||||
* Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the
|
* Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the
|
||||||
* implementation of actual widget type.
|
* implementation of actual widget type.
|
||||||
*/
|
*/
|
||||||
getActiveNoteDetailWidget(): Promise<ReactWrappedWidget>;
|
getActiveNoteDetailWidget(): Promise<NoteDetailWidget>;
|
||||||
/**
|
/**
|
||||||
* @returns returns a note path of active note or null if there isn't active note
|
* @returns returns a note path of active note or null if there isn't active note
|
||||||
*/
|
*/
|
||||||
|
|||||||
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.froca = froca;
|
||||||
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
|
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) {
|
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||||
const string = String(msg).toLowerCase();
|
const string = String(msg).toLowerCase();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
describe("i18n", () => {
|
describe("i18n", () => {
|
||||||
it("translations are valid JSON", () => {
|
it("translations are valid JSON", () => {
|
||||||
for (const locale of LOCALES) {
|
for (const locale of LOCALES) {
|
||||||
if (locale.contentOnly || locale.id === "en_rtl") {
|
if (locale.contentOnly) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import options from "./options.js";
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import i18nextHttpBackend from "i18next-http-backend";
|
import i18nextHttpBackend from "i18next-http-backend";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
|
import type { Locale } from "@triliumnext/commons";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
let locales: Locale[] | null;
|
let locales: Locale[] | null;
|
||||||
@@ -13,7 +13,7 @@ let locales: Locale[] | null;
|
|||||||
export let translationsInitializedPromise = $.Deferred();
|
export let translationsInitializedPromise = $.Deferred();
|
||||||
|
|
||||||
export async function initLocale() {
|
export async function initLocale() {
|
||||||
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
|
const locale = (options.get("locale") as string) || "en";
|
||||||
|
|
||||||
locales = await server.get<Locale[]>("options/locales");
|
locales = await server.get<Locale[]>("options/locales");
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ export async function initLocale() {
|
|||||||
returnEmptyString: false
|
returnEmptyString: false
|
||||||
});
|
});
|
||||||
|
|
||||||
await setDayjsLocale(locale);
|
|
||||||
translationsInitializedPromise.resolve();
|
translationsInitializedPromise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
|||||||
file: null,
|
file: null,
|
||||||
image: null,
|
image: null,
|
||||||
launcher: null,
|
launcher: null,
|
||||||
mermaid: "s1aBHPd79XYj",
|
mermaid: null,
|
||||||
mindMap: null,
|
mindMap: null,
|
||||||
noteMap: null,
|
noteMap: null,
|
||||||
relationMap: null,
|
relationMap: null,
|
||||||
@@ -27,8 +27,7 @@ export const byBookType: Record<ViewTypeOptions, string | null> = {
|
|||||||
calendar: "xWbu3jpNWapp",
|
calendar: "xWbu3jpNWapp",
|
||||||
table: "2FvYrpmOXm29",
|
table: "2FvYrpmOXm29",
|
||||||
geoMap: "81SGnPGMk7Xc",
|
geoMap: "81SGnPGMk7Xc",
|
||||||
board: "CtBQqbwXDx1w",
|
board: "CtBQqbwXDx1w"
|
||||||
presentation: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getHelpUrlForNote(note: FNote | null | undefined) {
|
export function getHelpUrlForNote(note: FNote | null | undefined) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
import shortcutService, { ShortcutBinding } from "./shortcuts.js";
|
import shortcutService from "./shortcuts.js";
|
||||||
import type Component from "../components/component.js";
|
import type Component from "../components/component.js";
|
||||||
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
|
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
|
||||||
|
|
||||||
@@ -28,24 +28,14 @@ async function getActionsForScope(scope: string) {
|
|||||||
return actions.filter((action) => action.scope === scope);
|
return actions.filter((action) => action.scope === scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component, ntxId: string | null | undefined) {
|
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
|
||||||
if (!$el[0]) return [];
|
|
||||||
|
|
||||||
const actions = await getActionsForScope(scope);
|
const actions = await getActionsForScope(scope);
|
||||||
const bindings: ShortcutBinding[] = [];
|
|
||||||
|
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
for (const shortcut of action.effectiveShortcuts ?? []) {
|
for (const shortcut of action.effectiveShortcuts ?? []) {
|
||||||
const binding = shortcutService.bindElShortcut($el, shortcut, () => {
|
shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
|
||||||
component.triggerCommand(action.actionName, { ntxId });
|
|
||||||
});
|
|
||||||
if (binding) {
|
|
||||||
bindings.push(binding);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return bindings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getActionsForScope("window").then((actions) => {
|
getActionsForScope("window").then((actions) => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import appContext, { type NoteCommandData } from "../components/app_context.js";
|
|||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
|
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
|
||||||
import { openInCurrentNoteContext } from "../components/note_context.js";
|
|
||||||
|
|
||||||
function getNotePathFromUrl(url: string) {
|
function getNotePathFromUrl(url: string) {
|
||||||
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
|
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
|
||||||
@@ -150,16 +149,11 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
|
|||||||
$container.append($noteLink);
|
$container.append($noteLink);
|
||||||
|
|
||||||
if (showNotePath) {
|
if (showNotePath) {
|
||||||
let pathSegments: string[];
|
|
||||||
if (notePath == "root") {
|
|
||||||
pathSegments = ["⌂"];
|
|
||||||
} else {
|
|
||||||
const resolvedPathSegments = (await treeService.resolveNotePathToSegments(notePath)) || [];
|
const resolvedPathSegments = (await treeService.resolveNotePathToSegments(notePath)) || [];
|
||||||
resolvedPathSegments.pop(); // Remove last element
|
resolvedPathSegments.pop(); // Remove last element
|
||||||
|
|
||||||
const resolvedPath = resolvedPathSegments.join("/");
|
const resolvedPath = resolvedPathSegments.join("/");
|
||||||
pathSegments = await treeService.getNotePathTitleComponents(resolvedPath);
|
const pathSegments = await treeService.getNotePathTitleComponents(resolvedPath);
|
||||||
}
|
|
||||||
|
|
||||||
if (pathSegments) {
|
if (pathSegments) {
|
||||||
if (pathSegments.length) {
|
if (pathSegments.length) {
|
||||||
@@ -285,7 +279,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
|
|||||||
* @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
|
* @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
|
||||||
* @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
|
* @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
|
||||||
*/
|
*/
|
||||||
export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
||||||
if (hrefLink?.startsWith("data:")) {
|
if (hrefLink?.startsWith("data:")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -307,8 +301,7 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
|
|||||||
// Right click is handled separately.
|
// Right click is handled separately.
|
||||||
const isMiddleClick = evt && "which" in evt && evt.which === 2;
|
const isMiddleClick = evt && "which" in evt && evt.which === 2;
|
||||||
const targetIsBlank = ($link?.attr("target") === "_blank");
|
const targetIsBlank = ($link?.attr("target") === "_blank");
|
||||||
const isDoubleClick = isLeftClick && evt?.type === "dblclick";
|
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
|
||||||
const openInNewTab = (isLeftClick && ctrlKey) || isDoubleClick || isMiddleClick || targetIsBlank;
|
|
||||||
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
||||||
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
|
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
|
||||||
|
|
||||||
@@ -323,28 +316,40 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
|
|||||||
viewScope
|
viewScope
|
||||||
});
|
});
|
||||||
} else if (isLeftClick) {
|
} 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) {
|
} else if (hrefLink) {
|
||||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||||
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
|
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
|
||||||
|
|
||||||
if (openInNewTab || openInNewWindow || (isLeftClick && (withinEditLink || outsideOfCKEditor))) {
|
if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
|
||||||
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
|
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
|
||||||
window.open(hrefLink, "_blank");
|
window.open(hrefLink, "_blank");
|
||||||
|
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
|
||||||
|
const electron = utils.dynamicRequire("electron");
|
||||||
|
electron.shell.openPath(hrefLink);
|
||||||
} else {
|
} else {
|
||||||
// Enable protocols supported by CKEditor 5 to be clickable.
|
// Enable protocols supported by CKEditor 5 to be clickable.
|
||||||
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
|
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
|
||||||
if ( utils.isElectron()) {
|
|
||||||
const electron = utils.dynamicRequire("electron");
|
|
||||||
electron.shell.openExternal(hrefLink);
|
|
||||||
} else {
|
|
||||||
window.open(hrefLink, "_blank");
|
window.open(hrefLink, "_blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -467,21 +472,29 @@ function getReferenceLinkTitleSync(href: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (glob.device !== "print") {
|
// TODO: Check why the event is not supported.
|
||||||
// TODO: Check why the event is not supported.
|
//@ts-ignore
|
||||||
//@ts-ignore
|
$(document).on("click", "a", goToLink);
|
||||||
$(document).on("click", "a", goToLink);
|
// TODO: Check why the event is not supported.
|
||||||
// TODO: Check why the event is not supported.
|
//@ts-ignore
|
||||||
//@ts-ignore
|
$(document).on("auxclick", "a", goToLink); // to handle the middle button
|
||||||
$(document).on("auxclick", "a", goToLink); // to handle the middle button
|
// TODO: Check why the event is not supported.
|
||||||
// TODO: Check why the event is not supported.
|
//@ts-ignore
|
||||||
//@ts-ignore
|
$(document).on("contextmenu", "a", linkContextMenu);
|
||||||
$(document).on("contextmenu", "a", linkContextMenu);
|
$(document).on("dblclick", "a", (e) => {
|
||||||
// TODO: Check why the event is not supported.
|
e.preventDefault();
|
||||||
//@ts-ignore
|
e.stopPropagation();
|
||||||
$(document).on("dblclick", "a", goToLink);
|
|
||||||
|
|
||||||
$(document).on("mousedown", "a", (e) => {
|
const $link = $(e.target).closest("a");
|
||||||
|
|
||||||
|
const address = $link.attr("href");
|
||||||
|
|
||||||
|
if (address && address.startsWith("http")) {
|
||||||
|
window.open(address, "_blank");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("mousedown", "a", (e) => {
|
||||||
if (e.which === 2) {
|
if (e.which === 2) {
|
||||||
// prevent paste on middle click
|
// prevent paste on middle click
|
||||||
// https://github.com/zadam/trilium/issues/2995
|
// https://github.com/zadam/trilium/issues/2995
|
||||||
@@ -489,8 +502,7 @@ if (glob.device !== "print") {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getNotePathFromUrl,
|
getNotePathFromUrl,
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
|
import type { AttachmentRow, EtapiTokenRow, OptionNames } from "@triliumnext/commons";
|
||||||
import type { AttributeType } from "../entities/fattribute.js";
|
import type { AttributeType } from "../entities/fattribute.js";
|
||||||
import type { EntityChange } from "../server_types.js";
|
import type { EntityChange } from "../server_types.js";
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
// TODO: Deduplicate with server.
|
||||||
|
|
||||||
interface NoteRow {
|
interface NoteRow {
|
||||||
blobId: string;
|
|
||||||
dateCreated: string;
|
|
||||||
dateModified: string;
|
|
||||||
isDeleted?: boolean;
|
isDeleted?: boolean;
|
||||||
isProtected?: boolean;
|
|
||||||
mime: string;
|
|
||||||
noteId: string;
|
|
||||||
title: string;
|
|
||||||
type: NoteType;
|
|
||||||
utcDateCreated: string;
|
|
||||||
utcDateModified: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Deduplicate with BranchRow from `rows.ts`/
|
// TODO: Deduplicate with BranchRow from `rows.ts`/
|
||||||
|
|||||||
@@ -168,8 +168,7 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const templateNote of childNotes) {
|
for (const templateNote of childNotes) {
|
||||||
if (templateNote.hasLabel("collection") !== filterCollections ||
|
if (templateNote.hasLabel("collection") !== filterCollections) {
|
||||||
!templateNote.hasLabel("template")) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import options from "./options.js";
|
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
|
|
||||||
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
|
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
|
||||||
@@ -127,7 +126,7 @@ function downloadRevision(noteId: string, revisionId: string) {
|
|||||||
/**
|
/**
|
||||||
* @param url - should be without initial slash!!!
|
* @param url - should be without initial slash!!!
|
||||||
*/
|
*/
|
||||||
export function getUrlForDownload(url: string) {
|
function getUrlForDownload(url: string) {
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
// electron needs absolute URL, so we extract current host, port, protocol
|
// electron needs absolute URL, so we extract current host, port, protocol
|
||||||
return `${getHost()}/${url}`;
|
return `${getHost()}/${url}`;
|
||||||
@@ -172,21 +171,6 @@ function getHost() {
|
|||||||
return `${url.protocol}//${url.hostname}:${url.port}`;
|
return `${url.protocol}//${url.hostname}:${url.port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openNoteOnServer(noteId: string) {
|
|
||||||
// Get the sync server host from options
|
|
||||||
const syncServerHost = options.get("syncServerHost");
|
|
||||||
|
|
||||||
if (!syncServerHost) {
|
|
||||||
console.error("No sync server host configured");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(`#root/${noteId}`, syncServerHost).toString();
|
|
||||||
|
|
||||||
// Use window.open to ensure link opens in external browser in Electron
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDirectory(directory: string) {
|
async function openDirectory(directory: string) {
|
||||||
try {
|
try {
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
@@ -214,6 +198,5 @@ export default {
|
|||||||
openAttachmentExternally,
|
openAttachmentExternally,
|
||||||
openNoteCustom,
|
openNoteCustom,
|
||||||
openAttachmentCustom,
|
openAttachmentCustom,
|
||||||
openNoteOnServer,
|
|
||||||
openDirectory
|
openDirectory
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,17 +41,6 @@ function parse(value: string) {
|
|||||||
return defObj;
|
return defObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For an attribute definition name (e.g. `label:TEST:TEST1`), extracts its type (label) and name (TEST:TEST1).
|
|
||||||
* @param definitionAttrName the attribute definition name, without the leading `#` (e.g. `label:TEST:TEST1`)
|
|
||||||
* @return a tuple of [type, name].
|
|
||||||
*/
|
|
||||||
export function extractAttributeDefinitionTypeAndName(definitionAttrName: string): [ "label" | "relation", string ] {
|
|
||||||
const valueType = definitionAttrName.startsWith("label:") ? "label" : "relation";
|
|
||||||
const valueName = definitionAttrName.substring(valueType.length + 1);
|
|
||||||
return [ valueType, valueName ];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse
|
parse
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import options from "./options.js";
|
import options from "./options.js";
|
||||||
import Split from "@triliumnext/split.js";
|
import Split from "split.js"
|
||||||
|
|
||||||
export const DEFAULT_GUTTER_SIZE = 5;
|
export const DEFAULT_GUTTER_SIZE = 5;
|
||||||
|
|
||||||
@@ -46,7 +46,6 @@ function setupLeftPaneResizer(leftPaneVisible: boolean) {
|
|||||||
sizes: [leftPaneWidth, restPaneWidth],
|
sizes: [leftPaneWidth, restPaneWidth],
|
||||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||||
minSize: [150, 300],
|
minSize: [150, 300],
|
||||||
rtl: glob.isRtl,
|
|
||||||
onDragEnd: (sizes) => {
|
onDragEnd: (sizes) => {
|
||||||
leftPaneWidth = Math.round(sizes[0]);
|
leftPaneWidth = Math.round(sizes[0]);
|
||||||
options.save("leftPaneWidth", Math.round(sizes[0]));
|
options.save("leftPaneWidth", Math.round(sizes[0]));
|
||||||
@@ -80,7 +79,6 @@ function setupRightPaneResizer() {
|
|||||||
sizes: [100 - rightPaneWidth, rightPaneWidth],
|
sizes: [100 - rightPaneWidth, rightPaneWidth],
|
||||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||||
minSize: [300, 180],
|
minSize: [300, 180],
|
||||||
rtl: glob.isRtl,
|
|
||||||
onDragEnd: (sizes) => {
|
onDragEnd: (sizes) => {
|
||||||
rightPaneWidth = Math.round(sizes[1]);
|
rightPaneWidth = Math.round(sizes[1]);
|
||||||
options.save("rightPaneWidth", Math.round(sizes[1]));
|
options.save("rightPaneWidth", Math.round(sizes[1]));
|
||||||
@@ -156,7 +154,6 @@ function createSplitInstance(targetNtxIds: string[]) {
|
|||||||
const splitPanels = [...splitNoteContainer.querySelectorAll<HTMLElement>(':scope > .note-split')]
|
const splitPanels = [...splitNoteContainer.querySelectorAll<HTMLElement>(':scope > .note-split')]
|
||||||
.filter(el => targetNtxIds.includes(el.getAttribute('data-ntx-id') ?? ""));
|
.filter(el => targetNtxIds.includes(el.getAttribute('data-ntx-id') ?? ""));
|
||||||
const splitInstance = Split(splitPanels, {
|
const splitInstance = Split(splitPanels, {
|
||||||
rtl: glob.isRtl,
|
|
||||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||||
minSize: 150,
|
minSize: 150,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe("shortcuts", () => {
|
|||||||
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
||||||
|
|
||||||
// Special keys
|
// Special keys
|
||||||
for (const keyCode of [ "Delete", "Enter", "NumpadEnter" ]) {
|
for (const keyCode of [ "Delete", "Enter" ]) {
|
||||||
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
||||||
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
|
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import utils from "./utils.js";
|
|||||||
type ElementType = HTMLElement | Document;
|
type ElementType = HTMLElement | Document;
|
||||||
type Handler = (e: KeyboardEvent) => void;
|
type Handler = (e: KeyboardEvent) => void;
|
||||||
|
|
||||||
export interface ShortcutBinding {
|
interface ShortcutBinding {
|
||||||
element: HTMLElement | Document;
|
element: HTMLElement | Document;
|
||||||
shortcut: string;
|
shortcut: string;
|
||||||
handler: Handler;
|
handler: Handler;
|
||||||
@@ -46,7 +46,6 @@ for (let i = 1; i <= 19; i++) {
|
|||||||
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
||||||
"Delete",
|
"Delete",
|
||||||
"Enter",
|
"Enter",
|
||||||
"NumpadEnter",
|
|
||||||
...functionKeyCodes
|
...functionKeyCodes
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -127,20 +126,10 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
|
|||||||
activeBindings.set(key, []);
|
activeBindings.set(key, []);
|
||||||
}
|
}
|
||||||
activeBindings.get(key)!.push(binding);
|
activeBindings.get(key)!.push(binding);
|
||||||
return binding;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeIndividualBinding(binding: ShortcutBinding) {
|
|
||||||
const key = binding.namespace ?? "global";
|
|
||||||
const activeBindingsInNamespace = activeBindings.get(key);
|
|
||||||
if (activeBindingsInNamespace) {
|
|
||||||
activeBindings.set(key, activeBindingsInNamespace.filter(aBinding => aBinding.handler === binding.handler));
|
|
||||||
}
|
|
||||||
binding.element.removeEventListener("keydown", binding.listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNamespaceBindings(namespace: string) {
|
function removeNamespaceBindings(namespace: string) {
|
||||||
const bindings = activeBindings.get(namespace);
|
const bindings = activeBindings.get(namespace);
|
||||||
if (bindings) {
|
if (bindings) {
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (glob.device !== "print") {
|
|
||||||
applyCopyToClipboardButton($(codeBlock));
|
applyCopyToClipboardButton($(codeBlock));
|
||||||
}
|
|
||||||
|
|
||||||
if (syntaxHighlightingEnabled) {
|
if (syntaxHighlightingEnabled) {
|
||||||
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
||||||
@@ -63,11 +61,7 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
|
|||||||
highlightedText = highlightAuto(text);
|
highlightedText = highlightAuto(text);
|
||||||
} else if (normalizedMimeType) {
|
} else if (normalizedMimeType) {
|
||||||
await ensureMimeTypesForHighlighting(normalizedMimeType);
|
await ensureMimeTypesForHighlighting(normalizedMimeType);
|
||||||
try {
|
|
||||||
highlightedText = highlight(text, { language: normalizedMimeType });
|
highlightedText = highlight(text, { language: normalizedMimeType });
|
||||||
} catch (e) {
|
|
||||||
console.warn("Unable to apply syntax highlight.", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (highlightedText) {
|
if (highlightedText) {
|
||||||
@@ -82,7 +76,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
|||||||
|
|
||||||
// Load theme.
|
// Load theme.
|
||||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||||
await loadHighlightingTheme(currentThemeName);
|
loadHighlightingTheme(currentThemeName);
|
||||||
|
|
||||||
// Load mime types.
|
// Load mime types.
|
||||||
let mimeTypes: MimeType[];
|
let mimeTypes: MimeType[];
|
||||||
@@ -104,16 +98,17 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
|||||||
highlightingLoaded = true;
|
highlightingLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadHighlightingTheme(themeName: string) {
|
export function loadHighlightingTheme(themeName: string) {
|
||||||
const themePrefix = "default:";
|
const themePrefix = "default:";
|
||||||
let theme: Theme | null = null;
|
let theme: Theme | null = null;
|
||||||
if (glob.device === "print") {
|
if (themeName.includes(themePrefix)) {
|
||||||
theme = Themes.vs;
|
|
||||||
} else if (themeName.includes(themePrefix)) {
|
|
||||||
theme = Themes[themeName.substring(themePrefix.length)];
|
theme = Themes[themeName.substring(themePrefix.length)];
|
||||||
}
|
}
|
||||||
|
if (!theme) {
|
||||||
|
theme = Themes.default;
|
||||||
|
}
|
||||||
|
|
||||||
await loadTheme(theme ?? Themes.default);
|
loadTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,50 +8,46 @@ export interface ToastOptions {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
autohide?: boolean;
|
autohide?: boolean;
|
||||||
closeAfter?: number;
|
closeAfter?: number;
|
||||||
progress?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toast({ title, icon, message, id, delay, autohide, progress }: ToastOptions) {
|
function toast(options: ToastOptions) {
|
||||||
const $toast = $(title
|
const $toast = $(options.title
|
||||||
? `\
|
? `\
|
||||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
<div class="toast-header">
|
<div class="toast-header">
|
||||||
<strong class="me-auto">
|
<strong class="me-auto">
|
||||||
<span class="bx bx-${icon}"></span>
|
<span class="bx bx-${options.icon}"></span>
|
||||||
<span class="toast-title"></span>
|
<span class="toast-title"></span>
|
||||||
</strong>
|
</strong>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-body"></div>
|
<div class="toast-body"></div>
|
||||||
<div class="toast-progress"></div>
|
|
||||||
</div>`
|
</div>`
|
||||||
: `
|
: `
|
||||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
<div class="toast-icon">
|
<div class="toast-icon">
|
||||||
<span class="bx bx-${icon}"></span>
|
<span class="bx bx-${options.icon}"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-body"></div>
|
<div class="toast-body"></div>
|
||||||
<div class="toast-header">
|
<div class="toast-header">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-progress"></div>
|
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
|
|
||||||
$toast.toggleClass("no-title", !title);
|
$toast.toggleClass("no-title", !options.title);
|
||||||
$toast.find(".toast-title").text(title ?? "");
|
$toast.find(".toast-title").text(options.title ?? "");
|
||||||
$toast.find(".toast-body").html(message);
|
$toast.find(".toast-body").html(options.message);
|
||||||
$toast.find(".toast-progress").css("width", `${(progress ?? 0) * 100}%`);
|
|
||||||
|
|
||||||
if (id) {
|
if (options.id) {
|
||||||
$toast.attr("id", `toast-${id}`);
|
$toast.attr("id", `toast-${options.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#toast-container").append($toast);
|
$("#toast-container").append($toast);
|
||||||
|
|
||||||
$toast.toast({
|
$toast.toast({
|
||||||
delay: delay || 3000,
|
delay: options.delay || 3000,
|
||||||
autohide: !!autohide
|
autohide: !!options.autohide
|
||||||
});
|
});
|
||||||
|
|
||||||
$toast.on("hidden.bs.toast", (e) => e.target.remove());
|
$toast.on("hidden.bs.toast", (e) => e.target.remove());
|
||||||
@@ -66,7 +62,6 @@ function showPersistent(options: ToastOptions) {
|
|||||||
|
|
||||||
if ($toast.length > 0) {
|
if ($toast.length > 0) {
|
||||||
$toast.find(".toast-body").html(options.message);
|
$toast.find(".toast-body").html(options.message);
|
||||||
$toast.find(".toast-progress").css("width", `${(options.progress ?? 0) * 100}%`);
|
|
||||||
} else {
|
} else {
|
||||||
options.autohide = false;
|
options.autohide = false;
|
||||||
|
|
||||||
@@ -82,11 +77,11 @@ function closePersistent(id: string) {
|
|||||||
$(`#toast-${id}`).remove();
|
$(`#toast-${id}`).remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMessage(message: string, delay = 2000, icon = "check") {
|
function showMessage(message: string, delay = 2000) {
|
||||||
console.debug(utils.now(), "message:", message);
|
console.debug(utils.now(), "message:", message);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
icon,
|
icon: "check",
|
||||||
message: message,
|
message: message,
|
||||||
autohide: true,
|
autohide: true,
|
||||||
delay
|
delay
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import froca from "./froca.js";
|
|||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
||||||
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||||
|
|
||||||
@@ -26,12 +29,21 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
}
|
}
|
||||||
|
|
||||||
const path = notePath.split("/").reverse();
|
const path = notePath.split("/").reverse();
|
||||||
|
|
||||||
|
if (!path.includes("root")) {
|
||||||
|
path.push("root");
|
||||||
|
}
|
||||||
|
|
||||||
const effectivePathSegments: string[] = [];
|
const effectivePathSegments: string[] = [];
|
||||||
let childNoteId: string | null = null;
|
let childNoteId: string | null = null;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
for (let i = 0; i < path.length; i++) {
|
while (true) {
|
||||||
const parentNoteId = path[i];
|
if (i >= path.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentNoteId = path[i++];
|
||||||
|
|
||||||
if (childNoteId !== null) {
|
if (childNoteId !== null) {
|
||||||
const child = await froca.getNote(childNoteId, !logErrors);
|
const child = await froca.getNote(childNoteId, !logErrors);
|
||||||
@@ -56,7 +68,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parents.some(p => p.noteId === parentNoteId) || (i === path.length - 1 && parentNoteId !== 'root')) {
|
if (!parents.some((p) => p.noteId === parentNoteId)) {
|
||||||
if (logErrors) {
|
if (logErrors) {
|
||||||
const parent = froca.getNoteFromCache(parentNoteId);
|
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);
|
||||||
const bestNotePath = child.getBestNotePath(hoistedNoteId, activeNotePath);
|
|
||||||
|
|
||||||
if (bestNotePath) {
|
if (bestNotePath) {
|
||||||
const pathToRoot = bestNotePath.reverse().slice(1);
|
const pathToRoot = bestNotePath.reverse().slice(1);
|
||||||
@@ -89,7 +100,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
|
|
||||||
effectivePathSegments.reverse();
|
effectivePathSegments.reverse();
|
||||||
|
|
||||||
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
|
if (effectivePathSegments.includes(hoistedNoteId)) {
|
||||||
return effectivePathSegments;
|
return effectivePathSegments;
|
||||||
} else {
|
} else {
|
||||||
const noteId = getNoteIdFromUrl(notePath);
|
const noteId = getNoteIdFromUrl(notePath);
|
||||||
@@ -100,9 +111,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
if (!note) {
|
if (!note) {
|
||||||
throw new Error(`Unable to find note: ${notePath}.`);
|
throw new Error(`Unable to find note: ${notePath}.`);
|
||||||
}
|
}
|
||||||
|
const bestNotePath = note.getBestNotePath(hoistedNoteId);
|
||||||
const activeNotePath = appContext.tabManager.getActiveContextNotePath();
|
|
||||||
const bestNotePath = note.getBestNotePath(hoistedNoteId, activeNotePath);
|
|
||||||
|
|
||||||
if (!bestNotePath) {
|
if (!bestNotePath) {
|
||||||
throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);
|
throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { dayjs } from "@triliumnext/commons";
|
import dayjs from "dayjs";
|
||||||
import type { ViewScope } from "./link.js";
|
import type { ViewScope } from "./link.js";
|
||||||
import FNote from "../entities/fnote";
|
import FNote from "../entities/fnote";
|
||||||
|
|
||||||
@@ -11,11 +11,7 @@ export function reloadFrontendApp(reason?: string) {
|
|||||||
logInfo(`Frontend app reload: ${reason}`);
|
logInfo(`Frontend app reload: ${reason}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isElectron()) {
|
|
||||||
dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow()?.reload();
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restartDesktopApp() {
|
export function restartDesktopApp() {
|
||||||
@@ -173,7 +169,7 @@ const entityMap: Record<string, string> = {
|
|||||||
"=": "="
|
"=": "="
|
||||||
};
|
};
|
||||||
|
|
||||||
export function escapeHtml(str: string) {
|
function escapeHtml(str: string) {
|
||||||
return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
|
return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +203,7 @@ function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function randomString(len: number) {
|
function randomString(len: number) {
|
||||||
let text = "";
|
let text = "";
|
||||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
@@ -274,7 +270,7 @@ function getMimeTypeClass(mime: string) {
|
|||||||
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isHtmlEmpty(html: string) {
|
function isHtmlEmpty(html: string) {
|
||||||
if (!html) {
|
if (!html) {
|
||||||
return true;
|
return true;
|
||||||
} else if (typeof html !== "string") {
|
} else if (typeof html !== "string") {
|
||||||
@@ -841,7 +837,7 @@ export function arrayEqual<T>(a: T[], b: T[]) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Indexed<T extends object> = T & { index: number };
|
type Indexed<T extends object> = T & { index: number };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an object array, alters every object in the array to have an index field assigned to it.
|
* Given an object array, alters every object in the array to have an index field assigned to it.
|
||||||
@@ -873,18 +869,6 @@ 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 {
|
export default {
|
||||||
reloadFrontendApp,
|
reloadFrontendApp,
|
||||||
restartDesktopApp,
|
restartDesktopApp,
|
||||||
|
|||||||
@@ -304,8 +304,6 @@ async function sendPing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (glob.device === "print") return;
|
|
||||||
|
|
||||||
ws = connectWebSocket();
|
ws = connectWebSocket();
|
||||||
|
|
||||||
lastPingTs = Date.now();
|
lastPingTs = Date.now();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "./stylesheets/auth.css";
|
import "./stylesheets/auth.css";
|
||||||
|
|
||||||
// @TriliumNextTODO: is this even needed anymore?
|
// @TriliumNextTODO: is this even needed anymore?
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import "jquery";
|
import "jquery";
|
||||||
import utils from "./services/utils.js";
|
import utils from "./services/utils.js";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
|
||||||
// TriliumNextTODO: properly make use of below types
|
// TriliumNextTODO: properly make use of below types
|
||||||
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
|
// 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/src/theme/ck-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() {
|
import mermaid from "mermaid";
|
||||||
const mermaidEls = document.querySelectorAll("#content pre code.language-mermaid");
|
|
||||||
if (mermaidEls.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mermaid = (await import("mermaid")).default;
|
export default function setupMermaid() {
|
||||||
|
for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) {
|
||||||
for (const codeBlock of mermaidEls) {
|
|
||||||
const parentPre = codeBlock.parentElement;
|
const parentPre = codeBlock.parentElement;
|
||||||
if (!parentPre) {
|
if (!parentPre) {
|
||||||
continue;
|
continue;
|
||||||
@@ -4,10 +4,6 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu:not(.static).calendar-dropdown-menu {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-dropdown-widget {
|
.calendar-dropdown-widget {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -64,7 +60,7 @@
|
|||||||
appearance: none;
|
appearance: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-inline-start: unset;
|
border-left: unset;
|
||||||
background-color: var(--menu-background-color);
|
background-color: var(--menu-background-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@@ -106,7 +102,7 @@
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
inset-inline-end: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background-color: var(--main-border-color);
|
background-color: var(--main-border-color);
|
||||||
|
|||||||
@@ -299,7 +299,7 @@
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
inset-inline-start: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, var(--hover-item-background-color, rgba(0, 0, 0, 0.03)), transparent);
|
background: linear-gradient(90deg, transparent, var(--hover-item-background-color, rgba(0, 0, 0, 0.03)), transparent);
|
||||||
@@ -406,10 +406,10 @@
|
|||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
inset-inline-start: -100%;
|
left: -100%;
|
||||||
}
|
}
|
||||||
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 .icon-action,
|
||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
.note-detail-relation-map {
|
.note-detail-relation-map {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
|
padding: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
.note-detail-relation-map .endpoint {
|
.note-detail-relation-map .endpoint {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 37%;
|
bottom: 37%;
|
||||||
inset-inline-end: 5px;
|
right: 5px;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -67,13 +67,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
||||||
margin-inline-start: var(--cell-editing-border-width);
|
margin-left: var(--cell-editing-border-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabulator div.tabulator-header .tabulator-col,
|
.tabulator div.tabulator-header .tabulator-col,
|
||||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
||||||
background: var(--col-header-background-color);
|
background: var(--col-header-background-color);
|
||||||
border-inline-end: var(--col-header-separator-border);
|
border-right: var(--col-header-separator-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table body */
|
/* Table body */
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabulator-row .tabulator-cell input {
|
.tabulator-row .tabulator-cell input {
|
||||||
padding-inline-start: var(--cell-horiz-padding-size) !important;
|
padding-left: var(--cell-horiz-padding-size) !important;
|
||||||
padding-inline-end: var(--cell-horiz-padding-size) !important;
|
padding-right: var(--cell-horiz-padding-size) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabulator-row {
|
.tabulator-row {
|
||||||
@@ -117,12 +117,12 @@
|
|||||||
/* Cell */
|
/* Cell */
|
||||||
|
|
||||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
||||||
margin-inline-end: var(--cell-editing-border-width);
|
margin-right: var(--cell-editing-border-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
|
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
|
||||||
.tabulator-row .tabulator-cell {
|
.tabulator-row .tabulator-cell {
|
||||||
border-inline-end-color: transparent;
|
border-right-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
|
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
|
||||||
@@ -156,14 +156,14 @@
|
|||||||
/* Align items without children/expander to the ones with. */
|
/* Align items without children/expander to the ones with. */
|
||||||
.tabulator-cell[tabulator-field="title"] > span:first-child, /* 1st level */
|
.tabulator-cell[tabulator-field="title"] > span:first-child, /* 1st level */
|
||||||
.tabulator-cell[tabulator-field="title"] > div:first-child + span { /* sub-level */
|
.tabulator-cell[tabulator-field="title"] > div:first-child + span { /* sub-level */
|
||||||
padding-inline-start: 21px;
|
padding-left: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox cells */
|
/* Checkbox cells */
|
||||||
|
|
||||||
.tabulator .tabulator-cell:has(svg),
|
.tabulator .tabulator-cell:has(svg),
|
||||||
.tabulator .tabulator-cell:has(input[type="checkbox"]) {
|
.tabulator .tabulator-cell:has(input[type="checkbox"]) {
|
||||||
padding-inline-start: 8px;
|
padding-left: 8px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -76,26 +76,12 @@
|
|||||||
|
|
||||||
--mermaid-theme: dark;
|
--mermaid-theme: dark;
|
||||||
--native-titlebar-background: #00000000;
|
--native-titlebar-background: #00000000;
|
||||||
|
|
||||||
--calendar-coll-event-background-saturation: 30%;
|
|
||||||
--calendar-coll-event-background-lightness: 30%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::-webkit-calendar-picker-indicator {
|
body ::-webkit-calendar-picker-indicator {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#left-pane .fancytree-node.tinted {
|
|
||||||
--custom-color: var(--dark-theme-custom-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root .reference-link,
|
|
||||||
:root .reference-link:hover,
|
|
||||||
.ck-content a.reference-link > span,
|
|
||||||
.board-note {
|
|
||||||
color: var(--dark-theme-custom-color, inherit);
|
|
||||||
}
|
|
||||||
|
|
||||||
.excalidraw.theme--dark {
|
.excalidraw.theme--dark {
|
||||||
--theme-filter: invert(80%) hue-rotate(180deg) !important;
|
--theme-filter: invert(80%) hue-rotate(180deg) !important;
|
||||||
}
|
}
|
||||||
@@ -111,7 +97,3 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
|||||||
.ck-content pre {
|
.ck-content pre {
|
||||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important;
|
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.use-note-color {
|
|
||||||
--custom-color: var(--dark-theme-custom-color);
|
|
||||||
}
|
|
||||||
@@ -80,22 +80,4 @@ html {
|
|||||||
|
|
||||||
--mermaid-theme: default;
|
--mermaid-theme: default;
|
||||||
--native-titlebar-background: #ffffff00;
|
--native-titlebar-background: #ffffff00;
|
||||||
|
|
||||||
--calendar-coll-event-background-lightness: 95%;
|
|
||||||
--calendar-coll-event-background-saturation: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#left-pane .fancytree-node.tinted {
|
|
||||||
--custom-color: var(--light-theme-custom-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root .reference-link,
|
|
||||||
:root .reference-link:hover,
|
|
||||||
.ck-content a.reference-link > span,
|
|
||||||
.board-note {
|
|
||||||
color: var(--light-theme-custom-color, inherit);
|
|
||||||
}
|
|
||||||
|
|
||||||
.use-note-color {
|
|
||||||
--custom-color: var(--light-theme-custom-color);
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user