mirror of
https://github.com/zadam/trilium.git
synced 2026-03-01 18:13:39 +01:00
Merge upstream changes and resolve conflicts
This commit is contained in:
commit
28dd85c1d1
@ -1,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{js,ts,tsx}]
|
||||
[*.{js,ts,tsx,css}]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
|
||||
4
.github/actions/build-electron/action.yml
vendored
4
.github/actions/build-electron/action.yml
vendored
@ -21,7 +21,7 @@ runs:
|
||||
# Certificate setup
|
||||
- name: Import Apple certificates
|
||||
if: inputs.os == 'macos'
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
|
||||
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
|
||||
@ -30,7 +30,7 @@ runs:
|
||||
|
||||
- name: Install Installer certificate
|
||||
if: inputs.os == 'macos'
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
|
||||
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
|
||||
|
||||
2
.github/actions/report-size/action.yml
vendored
2
.github/actions/report-size/action.yml
vendored
@ -44,7 +44,7 @@ runs:
|
||||
steps:
|
||||
# Checkout branch to compare to [required]
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
path: br-base
|
||||
|
||||
334
.github/copilot-instructions.md
vendored
Normal file
334
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,334 @@
|
||||
# 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)
|
||||
2
.github/workflows/codeql.yml
vendored
2
.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
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
|
||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
6
.github/workflows/dev.yml
vendored
6
.github/workflows/dev.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
needs:
|
||||
- test_dev
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@ -80,7 +80,7 @@ jobs:
|
||||
- dockerfile: Dockerfile
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Install dependencies
|
||||
|
||||
4
.github/workflows/main-docker.yml
vendored
4
.github/workflows/main-docker.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
||||
- dockerfile: Dockerfile
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set IMAGE_NAME to lowercase
|
||||
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
||||
@ -141,7 +141,7 @@ jobs:
|
||||
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
21
.github/workflows/nightly.yml
vendored
21
.github/workflows/nightly.yml
vendored
@ -45,9 +45,22 @@ jobs:
|
||||
image: win-signing
|
||||
shell: cmd
|
||||
forge_platform: win32
|
||||
# Exclude ARM64 Linux from default matrix to use native runner
|
||||
exclude:
|
||||
- arch: arm64
|
||||
os:
|
||||
name: linux
|
||||
# Add ARM64 Linux with native ubuntu-24.04-arm runner for better-sqlite3 compatibility
|
||||
include:
|
||||
- arch: arm64
|
||||
os:
|
||||
name: linux
|
||||
image: ubuntu-24.04-arm
|
||||
shell: bash
|
||||
forge_platform: linux
|
||||
runs-on: ${{ matrix.os.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
@ -77,7 +90,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
@ -109,7 +122,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Run the build
|
||||
uses: ./.github/actions/build-server
|
||||
@ -118,7 +131,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
|
||||
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
||||
TRILIUM_INTEGRATION_TEST: memory
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
@ -79,7 +79,7 @@ jobs:
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: e2e report
|
||||
name: e2e report ${{ matrix.arch }}
|
||||
path: apps/server-e2e/test-output
|
||||
|
||||
- name: Kill the server
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
forge_platform: linux
|
||||
runs-on: ${{ matrix.os.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
@ -91,7 +91,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Run the build
|
||||
uses: ./.github/actions/build-server
|
||||
@ -114,7 +114,7 @@ jobs:
|
||||
steps:
|
||||
- run: mkdir upload
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
docs/Release Notes
|
||||
@ -127,7 +127,7 @@ jobs:
|
||||
path: upload
|
||||
|
||||
- name: Publish stable release
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
uses: softprops/action-gh-release@v2.5.0
|
||||
with:
|
||||
draft: false
|
||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||
|
||||
2
.github/workflows/website.yml
vendored
2
.github/workflows/website.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
pull-requests: write # For PR preview comments
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -48,4 +48,5 @@ upload
|
||||
.svelte-kit
|
||||
|
||||
# docs
|
||||
site/
|
||||
site/
|
||||
apps/*/coverage
|
||||
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -9,7 +9,6 @@
|
||||
"tobermory.es6-string-html",
|
||||
"vitest.explorer",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"svelte.svelte-vscode",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
"usernamehw.errorlens"
|
||||
]
|
||||
}
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -36,5 +36,8 @@
|
||||
"docs/**/*.png": true,
|
||||
"apps/server/src/assets/doc_notes/**": true,
|
||||
"apps/edit-docs/demo/**": true
|
||||
}
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "*", "severity": "warn" }
|
||||
]
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import anonymizationService from "../src/services/anonymization.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
fs.writeFileSync(path.resolve(__dirname, "tpl", "anonymize-database.sql"), anonymizationService.getFullAnonymizationScript());
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCHEMA_FILE_PATH=db/schema.sql
|
||||
|
||||
sqlite3 ./data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH"
|
||||
|
||||
echo "DB schema exported to $SCHEMA_FILE_PATH"
|
||||
@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
SERIES=${VERSION:0:4}-latest
|
||||
|
||||
docker push zadam/trilium:$VERSION
|
||||
docker push zadam/trilium:$SERIES
|
||||
|
||||
if [[ $1 != *"beta"* ]]; then
|
||||
docker push zadam/trilium:latest
|
||||
fi
|
||||
@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
|
||||
then
|
||||
echo "Version ${VERSION} isn't in format X.Y.Z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_DATE=$(git log -1 --format=%aI "v${VERSION}" | cut -c -10)
|
||||
VERSION_COMMIT=$(git rev-list -n 1 "v${VERSION}")
|
||||
|
||||
# expecting the directory at a specific path
|
||||
cd ~/trilium-flathub || exit
|
||||
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo "There are uncommitted changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE_BRANCH=main
|
||||
|
||||
if [[ "$VERSION" == *"beta"* ]]; then
|
||||
BASE_BRANCH=beta
|
||||
fi
|
||||
|
||||
git switch "${BASE_BRANCH}"
|
||||
git pull
|
||||
|
||||
BRANCH=b${VERSION}
|
||||
|
||||
git branch "${BRANCH}"
|
||||
git switch "${BRANCH}"
|
||||
|
||||
echo "Updating files with version ${VERSION}, date ${VERSION_DATE} and commit ${VERSION_COMMIT}"
|
||||
|
||||
flatpak-node-generator npm ../trilium/package-lock.json
|
||||
|
||||
xmlstarlet ed --inplace --update "/component/releases/release/@version" --value "${VERSION}" --update "/component/releases/release/@date" --value "${VERSION_DATE}" ./com.github.zadam.trilium.metainfo.xml
|
||||
|
||||
yq --inplace "(.modules[0].sources[0].tag = \"v${VERSION}\") | (.modules[0].sources[0].commit = \"${VERSION_COMMIT}\")" ./com.github.zadam.trilium.yml
|
||||
|
||||
git add ./generated-sources.json
|
||||
git add ./com.github.zadam.trilium.metainfo.xml
|
||||
git add ./com.github.zadam.trilium.yml
|
||||
|
||||
git commit -m "release $VERSION"
|
||||
git push --set-upstream origin "${BRANCH}"
|
||||
|
||||
gh pr create --fill -B "${BASE_BRANCH}"
|
||||
gh pr merge --auto --merge --delete-branch
|
||||
@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Missing command: jq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
|
||||
then
|
||||
echo "Version ${VERSION} isn't in format X.Y.Z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo "There are uncommitted changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Releasing Trilium $VERSION"
|
||||
|
||||
jq '.version = "'$VERSION'"' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
git add package.json
|
||||
|
||||
npm run chore:update-build-info
|
||||
|
||||
git add src/services/build.ts
|
||||
|
||||
TAG=v$VERSION
|
||||
|
||||
echo "Committing package.json version change"
|
||||
|
||||
git commit -m "chore(release): $VERSION"
|
||||
git push
|
||||
|
||||
echo "Tagging commit with $TAG"
|
||||
|
||||
git tag $TAG
|
||||
git push origin $TAG
|
||||
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -1,51 +0,0 @@
|
||||
import eslint from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
// consider using rules below, once we have a full TS codebase and can be more strict
|
||||
// tseslint.configs.strictTypeChecked,
|
||||
// tseslint.configs.stylisticTypeChecked,
|
||||
// tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"simple-import-sort": simpleImportSort
|
||||
}
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
// add rule overrides here
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
"build/*",
|
||||
"dist/*",
|
||||
"docs/*",
|
||||
"demo/*",
|
||||
"src/public/app-dist/*",
|
||||
"src/public/app/doc_notes/*"
|
||||
]
|
||||
}
|
||||
);
|
||||
@ -1,47 +0,0 @@
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
|
||||
// eslint config just for formatting rules
|
||||
// potentially to be merged with the linting rules into one single config,
|
||||
// once we have fixed the majority of lint errors
|
||||
|
||||
// Go to https://eslint.style/rules/default/${rule_without_prefix} to check the rule details
|
||||
export const stylisticRules = {
|
||||
"@stylistic/indent": [ "error", 4 ],
|
||||
"@stylistic/quotes": [ "error", "double", { avoidEscape: true, allowTemplateLiterals: "always" } ],
|
||||
"@stylistic/semi": [ "error", "always" ],
|
||||
"@stylistic/quote-props": [ "error", "consistent-as-needed" ],
|
||||
"@stylistic/max-len": [ "error", { code: 100 } ],
|
||||
"@stylistic/comma-dangle": [ "error", "never" ],
|
||||
"@stylistic/linebreak-style": [ "error", "unix" ],
|
||||
"@stylistic/array-bracket-spacing": [ "error", "always" ],
|
||||
"@stylistic/object-curly-spacing": [ "error", "always" ],
|
||||
"@stylistic/padded-blocks": [ "error", { classes: "always" } ]
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
files: [ "**/*.{js,ts,mjs,cjs}" ],
|
||||
languageOptions: {
|
||||
parser: tsParser
|
||||
},
|
||||
plugins: {
|
||||
"@stylistic": stylistic
|
||||
},
|
||||
rules: {
|
||||
...stylisticRules
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
"build/*",
|
||||
"dist/*",
|
||||
"docs/*",
|
||||
"demo/*",
|
||||
// TriliumNextTODO: check if we want to format packages here as well - for now skipping it
|
||||
"packages/*",
|
||||
"src/public/app-dist/*",
|
||||
"src/public/app/doc_notes/*"
|
||||
]
|
||||
}
|
||||
];
|
||||
@ -1,17 +0,0 @@
|
||||
import { test as setup, expect } from "@playwright/test";
|
||||
|
||||
const authFile = "playwright/.auth/user.json";
|
||||
|
||||
const ROOT_URL = "http://localhost:8082";
|
||||
const LOGIN_PASSWORD = "demo1234";
|
||||
|
||||
// Reference: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
|
||||
|
||||
setup("authenticate", async ({ page }) => {
|
||||
await page.goto(ROOT_URL);
|
||||
await expect(page).toHaveURL(`${ROOT_URL}/login`);
|
||||
|
||||
await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD);
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
@ -1,9 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Can duplicate note with broken links", async ({ page }) => {
|
||||
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
|
||||
await page.locator(".tree-wrapper .fancytree-active").getByText("Note map").click({ button: "right" });
|
||||
await page.getByText("Duplicate subtree").click();
|
||||
await expect(page.locator(".toast-body")).toBeHidden();
|
||||
await expect(page.locator(".tree-wrapper").getByText("Note map (dup)")).toBeVisible();
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("has title", async ({ page }) => {
|
||||
await page.goto("https://playwright.dev/");
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test("get started link", async ({ page }) => {
|
||||
await page.goto("https://playwright.dev/");
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole("link", { name: "Get started" }).click();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
|
||||
});
|
||||
@ -1,21 +0,0 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
|
||||
test("Native Title Bar not displayed on web", async ({ page }) => {
|
||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsAppearance");
|
||||
await expect(page.getByRole("heading", { name: "Theme" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
|
||||
});
|
||||
|
||||
test("Tray settings not displayed on web", async ({ page }) => {
|
||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsOther");
|
||||
await expect(page.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||
});
|
||||
|
||||
test("Spellcheck settings not displayed on web", async ({ page }) => {
|
||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck");
|
||||
await expect(page.getByRole("heading", { name: "Spell Check" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||
await expect(page.getByText("These options apply only for desktop builds")).toBeVisible();
|
||||
await expect(page.getByText("Enable spellcheck")).toBeHidden();
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
|
||||
test("Renders on desktop", async ({ page, context }) => {
|
||||
await page.goto("http://localhost:8082");
|
||||
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
});
|
||||
|
||||
test("Renders on mobile", async ({ page, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
url: "http://localhost:8082",
|
||||
name: "trilium-device",
|
||||
value: "mobile"
|
||||
}
|
||||
]);
|
||||
await page.goto("http://localhost:8082");
|
||||
await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const expectedVersion = "0.90.3";
|
||||
|
||||
test("Displays update badge when there is a version available", async ({ page }) => {
|
||||
await page.goto("http://localhost:8080");
|
||||
await page.getByRole("button", { name: "" }).click();
|
||||
await page.getByText(`Version ${expectedVersion} is available,`).click();
|
||||
|
||||
const page1 = await page.waitForEvent("popup");
|
||||
expect(page1.url()).toBe(`https://github.com/TriliumNext/Trilium/releases/tag/v${expectedVersion}`);
|
||||
});
|
||||
@ -1,56 +0,0 @@
|
||||
{
|
||||
"main": "./electron-main.js",
|
||||
"bin": {
|
||||
"trilium": "src/main.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"server:start-safe": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nodemon src/main.ts",
|
||||
"server:start-no-dir": "cross-env TRILIUM_ENV=dev nodemon src/main.ts",
|
||||
"server:start-test": "npm run server:switch && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
|
||||
"server:qstart": "npm run server:switch && npm run server:start",
|
||||
"server:switch": "rimraf ./node_modules/better-sqlite3 && npm install",
|
||||
"electron:start-no-dir": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 electron --inspect=5858 .",
|
||||
"electron:start-nix": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
|
||||
"electron:start-nix-no-dir": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev TRILIUM_PORT=37742 nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
|
||||
"electron:start-prod-no-dir": "npm run build:prepare-dist && cross-env TRILIUM_ENV=prod electron --inspect=5858 .",
|
||||
"electron:start-prod-nix": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
||||
"electron:start-prod-nix-no-dir": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
||||
"electron:qstart": "npm run electron:switch && npm run electron:start",
|
||||
"electron:switch": "electron-rebuild",
|
||||
"docs:build": "typedoc",
|
||||
"test": "npm run client:test && npm run server:test",
|
||||
"client:test": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app",
|
||||
"client:coverage": "cross-env TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest --root src/public/app --coverage",
|
||||
"test:playwright": "playwright test --workers 1",
|
||||
"test:integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"test:integration-mem-db": "cross-env nodemon src/main.ts",
|
||||
"test:integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"dev:watch-dist": "tsx ./bin/watch-dist.ts",
|
||||
"dev:format-check": "eslint -c eslint.format.config.js .",
|
||||
"dev:format-fix": "eslint -c eslint.format.config.js . --fix",
|
||||
"dev:linter-check": "eslint .",
|
||||
"dev:linter-fix": "eslint . --fix",
|
||||
"chore:generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
|
||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.56.1",
|
||||
"@stylistic/eslint-plugin": "5.6.0",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/yargs": "17.0.35",
|
||||
"@vitest/coverage-v8": "4.0.10",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.5",
|
||||
"lorem-ipsum": "2.0.8",
|
||||
"rcedit": "5.0.1",
|
||||
"rimraf": "6.1.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"appdmg": "0.6.6"
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import etapi from "../support/etapi.js";
|
||||
/* TriliumNextTODO: port to Vitest
|
||||
etapi.describeEtapi("app_info", () => {
|
||||
it("get", async () => {
|
||||
const appInfo = await etapi.getEtapi("app-info");
|
||||
expect(appInfo.clipperProtocolVersion).toEqual("1.0");
|
||||
});
|
||||
});
|
||||
*/
|
||||
@ -1,10 +0,0 @@
|
||||
import etapi from "../support/etapi.js";
|
||||
|
||||
/* TriliumNextTODO: port to Vitest
|
||||
etapi.describeEtapi("backup", () => {
|
||||
it("create", async () => {
|
||||
const response = await etapi.putEtapiContent("backup/etapi_test");
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
});
|
||||
*/
|
||||
@ -1,26 +0,0 @@
|
||||
import etapi from "../support/etapi.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
/* TriliumNextTODO: port to Vitest
|
||||
etapi.describeEtapi("import", () => {
|
||||
// temporarily skip this test since test-export.zip is missing
|
||||
xit("import", async () => {
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const zipFileBuffer = fs.readFileSync(path.resolve(scriptDir, "test-export.zip"));
|
||||
|
||||
const response = await etapi.postEtapiContent("notes/root/import", zipFileBuffer);
|
||||
expect(response.status).toEqual(201);
|
||||
|
||||
const { note, branch } = await response.json();
|
||||
|
||||
expect(note.title).toEqual("test-export");
|
||||
expect(branch.parentNoteId).toEqual("root");
|
||||
|
||||
const content = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
||||
expect(content).toContain("test export content");
|
||||
});
|
||||
});
|
||||
*/
|
||||
@ -1,103 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
import etapi from "../support/etapi.js";
|
||||
|
||||
/* TriliumNextTODO: port to Vitest
|
||||
etapi.describeEtapi("notes", () => {
|
||||
it("create", async () => {
|
||||
const { note, branch } = await etapi.postEtapi("create-note", {
|
||||
parentNoteId: "root",
|
||||
type: "text",
|
||||
title: "Hello World!",
|
||||
content: "Content",
|
||||
prefix: "Custom prefix"
|
||||
});
|
||||
|
||||
expect(note.title).toEqual("Hello World!");
|
||||
expect(branch.parentNoteId).toEqual("root");
|
||||
expect(branch.prefix).toEqual("Custom prefix");
|
||||
|
||||
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
||||
expect(rNote.title).toEqual("Hello World!");
|
||||
|
||||
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
||||
expect(rContent).toEqual("Content");
|
||||
|
||||
const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`);
|
||||
expect(rBranch.parentNoteId).toEqual("root");
|
||||
expect(rBranch.prefix).toEqual("Custom prefix");
|
||||
});
|
||||
|
||||
it("patch", async () => {
|
||||
const { note } = await etapi.postEtapi("create-note", {
|
||||
parentNoteId: "root",
|
||||
type: "text",
|
||||
title: "Hello World!",
|
||||
content: "Content"
|
||||
});
|
||||
|
||||
await etapi.patchEtapi(`notes/${note.noteId}`, {
|
||||
title: "new title",
|
||||
type: "code",
|
||||
mime: "text/apl",
|
||||
dateCreated: "2000-01-01 12:34:56.999+0200",
|
||||
utcDateCreated: "2000-01-01 10:34:56.999Z"
|
||||
});
|
||||
|
||||
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
||||
expect(rNote.title).toEqual("new title");
|
||||
expect(rNote.type).toEqual("code");
|
||||
expect(rNote.mime).toEqual("text/apl");
|
||||
expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200");
|
||||
expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z");
|
||||
});
|
||||
|
||||
it("update content", async () => {
|
||||
const { note } = await etapi.postEtapi("create-note", {
|
||||
parentNoteId: "root",
|
||||
type: "text",
|
||||
title: "Hello World!",
|
||||
content: "Content"
|
||||
});
|
||||
|
||||
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content");
|
||||
|
||||
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
|
||||
expect(rContent).toEqual("new content");
|
||||
});
|
||||
|
||||
it("create / update binary content", async () => {
|
||||
const { note } = await etapi.postEtapi("create-note", {
|
||||
parentNoteId: "root",
|
||||
type: "file",
|
||||
title: "Hello World!",
|
||||
content: "ZZZ"
|
||||
});
|
||||
|
||||
const updatedContent = crypto.randomBytes(16);
|
||||
|
||||
await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
|
||||
|
||||
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).arrayBuffer();
|
||||
expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
|
||||
});
|
||||
|
||||
it("delete note", async () => {
|
||||
const { note } = await etapi.postEtapi("create-note", {
|
||||
parentNoteId: "root",
|
||||
type: "text",
|
||||
title: "Hello World!",
|
||||
content: "Content"
|
||||
});
|
||||
|
||||
await etapi.deleteEtapi(`notes/${note.noteId}`);
|
||||
|
||||
const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`);
|
||||
expect(resp.status).toEqual(404);
|
||||
|
||||
const error = await resp.json();
|
||||
expect(error.status).toEqual(404);
|
||||
expect(error.code).toEqual("NOTE_NOT_FOUND");
|
||||
expect(error.message).toEqual(`Note '${note.noteId}' not found.`);
|
||||
});
|
||||
});
|
||||
*/
|
||||
@ -1,152 +0,0 @@
|
||||
import { describe, beforeAll, afterAll } from "vitest";
|
||||
|
||||
let etapiAuthToken: string | undefined;
|
||||
|
||||
const getEtapiAuthorizationHeader = (): string => "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64");
|
||||
|
||||
const PORT: string = "9999";
|
||||
const HOST: string = "http://localhost:" + PORT;
|
||||
|
||||
type SpecDefinitionsFunc = () => void;
|
||||
|
||||
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
|
||||
describe(description, () => {
|
||||
beforeAll(async () => {});
|
||||
|
||||
afterAll(() => {});
|
||||
|
||||
specDefinitions();
|
||||
});
|
||||
}
|
||||
|
||||
async function getEtapiResponse(url: string): Promise<Response> {
|
||||
return await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getEtapi(url: string): Promise<any> {
|
||||
const response = await getEtapiResponse(url);
|
||||
return await processEtapiResponse(response);
|
||||
}
|
||||
|
||||
async function getEtapiContent(url: string): Promise<Response> {
|
||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
}
|
||||
});
|
||||
|
||||
checkStatus(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function postEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return await processEtapiResponse(response);
|
||||
}
|
||||
|
||||
async function postEtapiContent(url: string, data: BodyInit): Promise<Response> {
|
||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
},
|
||||
body: data
|
||||
});
|
||||
|
||||
checkStatus(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function putEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return await processEtapiResponse(response);
|
||||
}
|
||||
|
||||
async function putEtapiContent(url: string, data?: BodyInit): Promise<Response> {
|
||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
},
|
||||
body: data
|
||||
});
|
||||
|
||||
checkStatus(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function patchEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
|
||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return await processEtapiResponse(response);
|
||||
}
|
||||
|
||||
async function deleteEtapi(url: string): Promise<any> {
|
||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: getEtapiAuthorizationHeader()
|
||||
}
|
||||
});
|
||||
return await processEtapiResponse(response);
|
||||
}
|
||||
|
||||
async function processEtapiResponse(response: Response): Promise<any> {
|
||||
const text = await response.text();
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`ETAPI error ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
return text?.trim() ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
function checkStatus(response: Response): void {
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`ETAPI error ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
describeEtapi,
|
||||
getEtapi,
|
||||
getEtapiResponse,
|
||||
getEtapiContent,
|
||||
postEtapi,
|
||||
postEtapiContent,
|
||||
putEtapi,
|
||||
putEtapiContent,
|
||||
patchEtapi,
|
||||
deleteEtapi
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "./build",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["ES2023"],
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["./src/public/app/**/*"],
|
||||
"files": [
|
||||
"./src/public/app/types.d.ts",
|
||||
"./src/public/app/types-lib.d.ts",
|
||||
"./src/types.d.ts"
|
||||
]
|
||||
}
|
||||
@ -9,14 +9,14 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"packageManager": "pnpm@10.24.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.11.1",
|
||||
"@redocly/cli": "2.12.3",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"typedoc": "0.28.14",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"typedoc": "0.28.15",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import baseConfig from "../../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...baseConfig
|
||||
];
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.99.5",
|
||||
"version": "0.100.0",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@ -12,10 +12,10 @@
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest --coverage",
|
||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.39.1",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
@ -27,23 +27,24 @@
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mind-elixir/node-menu": "5.0.1",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@preact/signals": "2.5.1",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@zumer/snapdom": "2.0.1",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
"clsx": "2.1.1",
|
||||
"color": "5.0.3",
|
||||
"dayjs": "1.11.19",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "3.0.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "16.5.0",
|
||||
"i18next": "25.6.2",
|
||||
"i18next": "25.7.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@ -53,13 +54,13 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.0",
|
||||
"mermaid": "11.12.1",
|
||||
"mind-elixir": "5.3.6",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.3.7",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.2",
|
||||
"react-i18next": "16.3.3",
|
||||
"preact": "10.28.0",
|
||||
"react-i18next": "16.4.0",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
@ -73,10 +74,10 @@
|
||||
"@types/leaflet": "1.9.21",
|
||||
"@types/leaflet-gpx": "1.3.8",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/reveal.js": "5.2.1",
|
||||
"@types/reveal.js": "5.2.2",
|
||||
"@types/tabulator-tables": "6.3.0",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.0.10",
|
||||
"happy-dom": "20.0.11",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ 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";
|
||||
import type { InfoProps } from "../widgets/dialogs/info.jsx";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||
@ -124,7 +125,7 @@ export type CommandMappings = {
|
||||
isNewNote?: boolean;
|
||||
};
|
||||
showPromptDialog: PromptDialogOptions;
|
||||
showInfoDialog: ConfirmWithMessageOptions;
|
||||
showInfoDialog: InfoProps;
|
||||
showConfirmDialog: ConfirmWithMessageOptions;
|
||||
showRecentChanges: CommandData & { ancestorNoteId: string };
|
||||
showImportDialog: CommandData & { noteId: string };
|
||||
@ -445,6 +446,7 @@ type EventMappings = {
|
||||
error: string;
|
||||
};
|
||||
searchRefreshed: { ntxId?: string | null };
|
||||
textEditorRefreshed: { ntxId?: string | null, editor: CKTextEditor };
|
||||
hoistedNoteChanged: {
|
||||
noteId: string;
|
||||
ntxId: string | null;
|
||||
@ -486,7 +488,7 @@ type EventMappings = {
|
||||
relationMapResetPanZoom: { ntxId: string | null | undefined };
|
||||
relationMapResetZoomIn: { ntxId: string | null | undefined };
|
||||
relationMapResetZoomOut: { ntxId: string | null | undefined };
|
||||
activeNoteChanged: {};
|
||||
activeNoteChanged: {ntxId: string | null | undefined};
|
||||
showAddLinkDialog: AddLinkOpts;
|
||||
showIncludeDialog: IncludeNoteOpts;
|
||||
openBulkActionsDialog: {
|
||||
|
||||
@ -321,6 +321,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.type === "search") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ export default class TabManager extends Component {
|
||||
const activeNoteContext = this.getActiveContext();
|
||||
this.updateDocumentTitle(activeNoteContext);
|
||||
|
||||
this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event
|
||||
this.triggerEvent("activeNoteChanged", {ntxId:activeNoteContext?.ntxId}); // trigger this even in on popstate event
|
||||
}
|
||||
|
||||
calculateHash(): string {
|
||||
|
||||
@ -22,6 +22,7 @@ bundleService.getWidgetBundlesByParent().then(async (widgetBundles) => {
|
||||
appContext.setLayout(new DesktopLayout(widgetBundles));
|
||||
appContext.start().catch((e) => {
|
||||
toastService.showPersistent({
|
||||
id: "critical-error",
|
||||
title: t("toast.critical-error.title"),
|
||||
icon: "alert",
|
||||
message: t("toast.critical-error.message", { message: e.message })
|
||||
@ -58,6 +59,7 @@ function initOnElectron() {
|
||||
|
||||
initDarkOrLightMode(style);
|
||||
initTransparencyEffects(style, currentWindow);
|
||||
initFullScreenDetection(currentWindow);
|
||||
|
||||
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||
initTitleBarButtons(style, currentWindow);
|
||||
@ -87,6 +89,11 @@ 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) {
|
||||
if (window.glob.platform === "win32") {
|
||||
const material = style.getPropertyValue("--background-material");
|
||||
|
||||
@ -240,7 +240,7 @@ export default class FNote {
|
||||
|
||||
const aNote = this.froca.getNoteFromCache(aNoteId);
|
||||
|
||||
if (aNote.isArchived || aNote.isHiddenCompletely()) {
|
||||
if (!aNote || aNote.isArchived || aNote.isHiddenCompletely()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ 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";
|
||||
@ -21,7 +20,6 @@ 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 PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
@ -31,7 +29,6 @@ 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";
|
||||
@ -44,6 +41,9 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import utils from "../services/utils.js";
|
||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@ -124,7 +124,7 @@ export default class DesktopLayout {
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(new SpacerWidget(0, 1))
|
||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
.child(<ClosePaneButton />)
|
||||
@ -140,7 +140,7 @@ export default class DesktopLayout {
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfo />)
|
||||
)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(<PromotedAttributes />)
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
@ -184,14 +184,14 @@ export default class DesktopLayout {
|
||||
launcherPane = new FlexContainer("row")
|
||||
.css("height", "53px")
|
||||
.class("horizontal")
|
||||
.child(new LauncherContainer(true))
|
||||
.child(<LauncherContainer isHorizontalLayout={true} />)
|
||||
.child(<GlobalMenu isHorizontalLayout={true} />);
|
||||
} else {
|
||||
launcherPane = new FlexContainer("column")
|
||||
.css("width", "53px")
|
||||
.class("vertical")
|
||||
.child(<GlobalMenu isHorizontalLayout={false} />)
|
||||
.child(new LauncherContainer(false))
|
||||
.child(<LauncherContainer isHorizontalLayout={false} />)
|
||||
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
||||
}
|
||||
|
||||
|
||||
@ -22,16 +22,9 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.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 CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
|
||||
import ToastContainer from "../widgets/Toast.jsx";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
rootContainer
|
||||
@ -57,16 +50,7 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(<ConfirmDialog />)
|
||||
.child(<PromptDialog />)
|
||||
.child(<IncorrectCpuArchDialog />)
|
||||
.child(new PopupEditorDialog()
|
||||
.child(new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />))
|
||||
.child(<StandaloneRibbonAdapter component={FormattingToolbar} />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" displayOnlyCollections />))
|
||||
.child(<CallToActionDialog />);
|
||||
.child(<PopupEditorDialog />)
|
||||
.child(<CallToActionDialog />)
|
||||
.child(<ToastContainer />)
|
||||
}
|
||||
|
||||
@ -6,14 +6,12 @@ import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
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 ContentHeader from "../widgets/containers/content_header.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
@ -29,9 +27,13 @@ import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
span.keyboard-shortcut,
|
||||
kbd {
|
||||
display: none;
|
||||
}
|
||||
@ -141,33 +143,35 @@ export default class MobileLayout {
|
||||
.id("detail-container")
|
||||
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
|
||||
.child(
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfoWidget />)
|
||||
)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||
.child(<SearchResult />)
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
new SplitNoteContainer(() =>
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(<PromotedAttributes />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfoWidget />)
|
||||
)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||
.child(<SearchResult />)
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -179,7 +183,7 @@ export default class MobileLayout {
|
||||
.child(new FlexContainer("row")
|
||||
.class("horizontal")
|
||||
.css("height", "53px")
|
||||
.child(new LauncherContainer(true))
|
||||
.child(<LauncherContainer isHorizontalLayout />)
|
||||
.child(<GlobalMenuWidget isHorizontalLayout />)
|
||||
.id("launcher-pane"))
|
||||
)
|
||||
|
||||
@ -2,7 +2,7 @@ import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { should } from "vitest";
|
||||
import { h, JSX, render } from "preact";
|
||||
|
||||
export interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
@ -15,6 +15,11 @@ export interface ContextMenuOptions<T> {
|
||||
onHide?: () => void;
|
||||
}
|
||||
|
||||
export interface CustomMenuItem {
|
||||
kind: "custom",
|
||||
componentFn: () => JSX.Element | null;
|
||||
}
|
||||
|
||||
export interface MenuSeparatorItem {
|
||||
kind: "separator";
|
||||
}
|
||||
@ -51,7 +56,7 @@ export interface MenuCommandItem<T> {
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader;
|
||||
export type MenuItem<T> = MenuCommandItem<T> | CustomMenuItem | MenuSeparatorItem | MenuHeader;
|
||||
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||
|
||||
@ -160,16 +165,19 @@ class ContextMenu {
|
||||
let $group = $parent; // The current group or parent element to which items are being appended
|
||||
let shouldStartNewGroup = false; // If true, the next item will start a new group
|
||||
let shouldResetGroup = false; // If true, the next item will be the last one from the group
|
||||
let prevItemKind: string = "";
|
||||
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index];
|
||||
const itemKind = ("kind" in item) ? item.kind : "";
|
||||
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the current item is a header, start a new group. This group will contain the
|
||||
// header and the next item that follows the header.
|
||||
if ("kind" in item && item.kind === "header") {
|
||||
if (itemKind === "header") {
|
||||
if (multicolumn && !shouldResetGroup) {
|
||||
shouldStartNewGroup = true;
|
||||
}
|
||||
@ -195,125 +203,25 @@ class ContextMenu {
|
||||
shouldStartNewGroup = false;
|
||||
}
|
||||
|
||||
if ("kind" in item && item.kind === "separator") {
|
||||
if (itemKind === "separator") {
|
||||
if (prevItemKind === "separator") {
|
||||
// Skip consecutive separators
|
||||
continue;
|
||||
}
|
||||
$group.append($("<div>").addClass("dropdown-divider"));
|
||||
shouldResetGroup = true; // End the group after the next item
|
||||
} else if ("kind" in item && item.kind === "header") {
|
||||
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
|
||||
} else if (itemKind === "header") {
|
||||
$group.append($("<h6>").addClass("dropdown-header").text((item as MenuHeader).title));
|
||||
shouldResetGroup = true;
|
||||
} else {
|
||||
const $icon = $("<span>");
|
||||
|
||||
if ("uiIcon" in item || "checked" in item) {
|
||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||
if (icon) {
|
||||
$icon.addClass(icon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
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>));
|
||||
}
|
||||
|
||||
const $link = $("<span>")
|
||||
.append($icon)
|
||||
.append(" ") // some space between icon and text
|
||||
.append(item.title);
|
||||
|
||||
if ("badges" in item && item.badges) {
|
||||
for (let badge of item.badges) {
|
||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||
|
||||
if (badge.className) {
|
||||
badgeElement.addClass(badge.className);
|
||||
}
|
||||
|
||||
$link.append(badgeElement);
|
||||
}
|
||||
}
|
||||
|
||||
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
||||
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
|
||||
if (shortcuts) {
|
||||
const allShortcuts: string[] = [];
|
||||
for (const effectiveShortcut of shortcuts) {
|
||||
allShortcuts.push(effectiveShortcut.split("+")
|
||||
.map(key => `<kbd>${key}</kbd>`)
|
||||
.join("+"));
|
||||
}
|
||||
|
||||
if (allShortcuts.length) {
|
||||
const container = $("<span>").addClass("keyboard-shortcut");
|
||||
container.append($(allShortcuts.join(",")));
|
||||
$link.append(container);
|
||||
}
|
||||
}
|
||||
} else if ("shortcut" in item && item.shortcut) {
|
||||
$link.append($("<kbd>").text(item.shortcut));
|
||||
}
|
||||
|
||||
const $item = $("<li>")
|
||||
.addClass("dropdown-item")
|
||||
.append($link)
|
||||
.on("contextmenu", (e) => false)
|
||||
// important to use mousedown instead of click since the former does not change focus
|
||||
// (especially important for focused text for spell check)
|
||||
.on("mousedown", (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.which !== 1) {
|
||||
// only left click triggers menu items
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isMobile && "items" in item && item.items) {
|
||||
const $item = $(e.target).closest(".dropdown-item");
|
||||
|
||||
$item.toggleClass("submenu-open");
|
||||
$item.find("ul.dropdown-menu").toggleClass("show");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("handler" in item && item.handler) {
|
||||
item.handler(item, e);
|
||||
}
|
||||
|
||||
this.options?.selectMenuItemHandler(item, e);
|
||||
|
||||
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
||||
// might be handled again by top-level menu
|
||||
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) {
|
||||
$item.addClass("disabled");
|
||||
}
|
||||
|
||||
if ("items" in item && item.items) {
|
||||
$item.addClass("dropdown-submenu");
|
||||
$link.addClass("dropdown-toggle");
|
||||
|
||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||
const hasColumns = !!item.columns && item.columns > 1;
|
||||
if (!this.isMobile && hasColumns) {
|
||||
$subMenu.css("column-count", item.columns!);
|
||||
}
|
||||
|
||||
this.addItems($subMenu, item.items, hasColumns);
|
||||
|
||||
$item.append($subMenu);
|
||||
}
|
||||
|
||||
$group.append($item);
|
||||
|
||||
// After adding a menu item, if the previous item was a separator or header,
|
||||
// reset the group so that the next item will be appended directly to the parent.
|
||||
if (shouldResetGroup) {
|
||||
@ -321,9 +229,126 @@ class ContextMenu {
|
||||
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>");
|
||||
|
||||
if ("uiIcon" in item || "checked" in item) {
|
||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||
if (icon) {
|
||||
$icon.addClass(icon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
const $link = $("<span>")
|
||||
.append($icon)
|
||||
.append(" ") // some space between icon and text
|
||||
.append(item.title);
|
||||
|
||||
if ("badges" in item && item.badges) {
|
||||
for (let badge of item.badges) {
|
||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||
|
||||
if (badge.className) {
|
||||
badgeElement.addClass(badge.className);
|
||||
}
|
||||
|
||||
$link.append(badgeElement);
|
||||
}
|
||||
}
|
||||
|
||||
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
||||
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
|
||||
if (shortcuts) {
|
||||
const allShortcuts: string[] = [];
|
||||
for (const effectiveShortcut of shortcuts) {
|
||||
allShortcuts.push(effectiveShortcut.split("+")
|
||||
.map(key => `<kbd>${key}</kbd>`)
|
||||
.join("+"));
|
||||
}
|
||||
|
||||
if (allShortcuts.length) {
|
||||
const container = $("<span>").addClass("keyboard-shortcut");
|
||||
container.append($(allShortcuts.join(",")));
|
||||
$link.append(container);
|
||||
}
|
||||
}
|
||||
} else if ("shortcut" in item && item.shortcut) {
|
||||
$link.append($("<kbd>").text(item.shortcut));
|
||||
}
|
||||
|
||||
const $item = $("<li>")
|
||||
.addClass("dropdown-item")
|
||||
.append($link)
|
||||
.on("contextmenu", (e) => false)
|
||||
// important to use mousedown instead of click since the former does not change focus
|
||||
// (especially important for focused text for spell check)
|
||||
.on("mousedown", (e) => {
|
||||
if (e.which !== 1) {
|
||||
// only left click triggers menu items
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isMobile && "items" in item && item.items) {
|
||||
const $item = $(e.target).closest(".dropdown-item");
|
||||
|
||||
$item.toggleClass("submenu-open");
|
||||
$item.find("ul.dropdown-menu").toggleClass("show");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent submenu from failing to expand on mobile
|
||||
if (!("items" in item && item.items)) {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
if ("handler" in item && item.handler) {
|
||||
item.handler(item, e);
|
||||
}
|
||||
|
||||
this.options?.selectMenuItemHandler(item, e);
|
||||
|
||||
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
||||
// might be handled again by top-level menu
|
||||
return false;
|
||||
});
|
||||
|
||||
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
||||
$item.addClass("disabled");
|
||||
}
|
||||
|
||||
if ("items" in item && item.items) {
|
||||
$item.addClass("dropdown-submenu");
|
||||
$link.addClass("dropdown-toggle");
|
||||
|
||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||
const hasColumns = !!item.columns && item.columns > 1;
|
||||
if (!this.isMobile && hasColumns) {
|
||||
$subMenu.css("column-count", item.columns!);
|
||||
}
|
||||
|
||||
this.addItems($subMenu, item.items, hasColumns);
|
||||
|
||||
$item.append($subMenu);
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
async hide() {
|
||||
this.options?.onHide?.();
|
||||
this.$widget.removeClass("show");
|
||||
|
||||
21
apps/client/src/menus/context_menu_utils.ts
Normal file
21
apps/client/src/menus/context_menu_utils.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
apps/client/src/menus/custom-items/NoteColorPicker.css
Normal file
86
apps/client/src/menus/custom-items/NoteColorPicker.css
Normal file
@ -0,0 +1,86 @@
|
||||
: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;
|
||||
}
|
||||
204
apps/client/src/menus/custom-items/NoteColorPicker.tsx
Normal file
204
apps/client/src/menus/custom-items/NoteColorPicker.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
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;
|
||||
}
|
||||
BIN
apps/client/src/menus/custom-items/custom-color.png
Normal file
BIN
apps/client/src/menus/custom-items/custom-color.png
Normal file
Binary file not shown.
|
After 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 { t } from "../services/i18n.js";
|
||||
import type { BrowserWindow } from "electron";
|
||||
import type { CommandNames } from "../components/app_context.js";
|
||||
import type { CommandNames, AppContext } from "../components/app_context.js";
|
||||
|
||||
function setupContextMenu() {
|
||||
const electron = utils.dynamicRequire("electron");
|
||||
@ -13,6 +13,8 @@ function setupContextMenu() {
|
||||
// FIXME: Remove typecast once Electron is properly integrated.
|
||||
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
||||
|
||||
let appContext: AppContext;
|
||||
|
||||
webContents.on("context-menu", (event, params) => {
|
||||
const { editFlags } = params;
|
||||
const hasText = params.selectionText.trim().length > 0;
|
||||
@ -119,6 +121,20 @@ function setupContextMenu() {
|
||||
uiIcon: "bx bx-search-alt",
|
||||
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) {
|
||||
|
||||
@ -2,26 +2,32 @@ import { t } from "../services/i18n.js";
|
||||
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
||||
import appContext, { type CommandNames } from "../components/app_context.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) {
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: getItems(),
|
||||
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
|
||||
items: getItems(e),
|
||||
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, e, notePath, viewScope, hoistedNoteId)
|
||||
});
|
||||
}
|
||||
|
||||
function getItems(): MenuItem<CommandNames>[] {
|
||||
function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
|
||||
const ntxId = getNtxId(e);
|
||||
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
|
||||
|
||||
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_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||
{ 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_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
||||
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
||||
];
|
||||
}
|
||||
|
||||
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||
if (!hoistedNoteId) {
|
||||
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
|
||||
}
|
||||
@ -29,15 +35,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
|
||||
if (command === "openNoteInNewTab") {
|
||||
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewSplit") {
|
||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||
|
||||
if (!subContexts) {
|
||||
logError("subContexts is null");
|
||||
return;
|
||||
}
|
||||
|
||||
const { ntxId } = subContexts[subContexts.length - 1];
|
||||
|
||||
const ntxId = getNtxId(e);
|
||||
if (!ntxId) return;
|
||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewWindow") {
|
||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||
@ -46,6 +45,18 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
getItems,
|
||||
handleLinkContextMenuItem,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
||||
import treeService from "../services/tree.js";
|
||||
import froca from "../services/froca.js";
|
||||
import clipboard from "../services/clipboard.js";
|
||||
@ -139,7 +140,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
uiIcon: "bx bx-rename",
|
||||
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
||||
},
|
||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||
{
|
||||
title:
|
||||
t("tree-context-menu.convert-to-attachment"),
|
||||
command: "convertNoteToAttachment",
|
||||
uiIcon: "bx bx-paperclip",
|
||||
enabled: isNotRoot && !isHoisted && notOptionsOrHelp && selectedNotes.some(note => note.isEligibleForConversionToAttachment())
|
||||
},
|
||||
|
||||
{ kind: "separator" },
|
||||
|
||||
@ -241,6 +248,15 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
|
||||
},
|
||||
|
||||
{ kind: "separator"},
|
||||
|
||||
(notOptionsOrHelp && selectedNotes.length === 1) ? {
|
||||
kind: "custom",
|
||||
componentFn: () => {
|
||||
return NoteColorPicker({note});
|
||||
}
|
||||
} : null,
|
||||
|
||||
{ kind: "separator" },
|
||||
|
||||
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
||||
|
||||
@ -3,10 +3,13 @@ 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() {
|
||||
@ -23,13 +26,21 @@ async function main() {
|
||||
|
||||
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 };
|
||||
const props: RendererProps | undefined | null = note && { note, onReady, onProgressChanged };
|
||||
|
||||
if (!note || !props) return <Error404 noteId={noteId} />
|
||||
|
||||
@ -71,6 +82,11 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize mermaid.
|
||||
if (note.type === "text") {
|
||||
await applyInlineMermaid(container);
|
||||
}
|
||||
|
||||
// Check custom CSS.
|
||||
await loadCustomCss(note);
|
||||
}
|
||||
@ -84,7 +100,7 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
||||
</>;
|
||||
}
|
||||
|
||||
function CollectionRenderer({ note, onReady }: RendererProps) {
|
||||
function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps) {
|
||||
const viewType = useNoteViewType(note);
|
||||
return <CustomNoteList
|
||||
viewType={viewType}
|
||||
@ -98,6 +114,7 @@ function CollectionRenderer({ note, onReady }: RendererProps) {
|
||||
await loadCustomCss(note);
|
||||
onReady();
|
||||
}}
|
||||
onProgressChanged={onProgressChanged}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
||||
@ -126,9 +126,7 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This doesn't seem right.
|
||||
//@ts-ignore
|
||||
if (this.isInheritable) {
|
||||
if (attrRow.isInheritable) {
|
||||
for (const owningNote of owningNotes) {
|
||||
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
||||
return true;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import utils from "./utils.js";
|
||||
import server from "./server.js";
|
||||
import toastService, { type ToastOptions } from "./toast.js";
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import ws from "./ws.js";
|
||||
@ -195,11 +195,11 @@ function filterRootNote(branchIds: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
function makeToast(id: string, message: string): ToastOptions {
|
||||
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
|
||||
return {
|
||||
id: id,
|
||||
id,
|
||||
title: t("branches.delete-status"),
|
||||
message: message,
|
||||
message,
|
||||
icon: "trash"
|
||||
};
|
||||
}
|
||||
@ -216,7 +216,7 @@ ws.subscribeToMessages(async (message) => {
|
||||
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
|
||||
} else if (message.type === "taskSucceeded") {
|
||||
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
|
||||
toast.closeAfter = 5000;
|
||||
toast.timeout = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
}
|
||||
@ -234,7 +234,7 @@ ws.subscribeToMessages(async (message) => {
|
||||
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
|
||||
} else if (message.type === "taskSucceeded") {
|
||||
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
|
||||
toast.closeAfter = 5000;
|
||||
toast.timeout = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
}
|
||||
@ -242,7 +242,7 @@ ws.subscribeToMessages(async (message) => {
|
||||
|
||||
async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
|
||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
||||
prefix: prefix
|
||||
prefix
|
||||
});
|
||||
|
||||
if (!resp.success) {
|
||||
@ -252,7 +252,7 @@ async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, pr
|
||||
|
||||
async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix?: string) {
|
||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
||||
prefix: prefix
|
||||
prefix
|
||||
});
|
||||
|
||||
if (!resp.success) {
|
||||
|
||||
44
apps/client/src/services/bundle.spec.ts
Normal file
44
apps/client/src/services/bundle.spec.ts
Normal file
@ -0,0 +1,44 @@
|
||||
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);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
||||
export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
||||
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
||||
|
||||
try {
|
||||
@ -36,10 +36,17 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
|
||||
}.call(apiContext);
|
||||
} catch (e: any) {
|
||||
const note = await froca.getNote(bundle.noteId);
|
||||
|
||||
const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
|
||||
showError(message);
|
||||
logError(message);
|
||||
toastService.showPersistent({
|
||||
id: `custom-script-failure-${note?.noteId}`,
|
||||
title: t("toast.bundle-error.title"),
|
||||
icon: "bx bx-error-circle",
|
||||
message: t("toast.bundle-error.message", {
|
||||
id: note?.noteId,
|
||||
title: note?.title,
|
||||
message: e.message
|
||||
})
|
||||
});
|
||||
logError("Widget initialization failed: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,8 +109,9 @@ async function getWidgetBundlesByParent() {
|
||||
const noteId = bundle.noteId;
|
||||
const note = await froca.getNote(noteId);
|
||||
toastService.showPersistent({
|
||||
id: `custom-script-failure-${noteId}`,
|
||||
title: t("toast.bundle-error.title"),
|
||||
icon: "alert",
|
||||
icon: "bx bx-error-circle",
|
||||
message: t("toast.bundle-error.message", {
|
||||
id: noteId,
|
||||
title: note?.title,
|
||||
|
||||
@ -2,24 +2,21 @@ import renderService from "./render.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import openService from "./open.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import linkService from "./link.js";
|
||||
import treeService from "./tree.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js";
|
||||
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
|
||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||
import renderDoc from "./doc_renderer.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
import { renderMathInElement } from "./math.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import renderText from "./content_renderer_text.js";
|
||||
|
||||
let idCounter = 1;
|
||||
|
||||
interface Options {
|
||||
export interface RenderOptions {
|
||||
tooltip?: boolean;
|
||||
trim?: boolean;
|
||||
imageHasZoom?: boolean;
|
||||
@ -29,7 +26,7 @@ interface Options {
|
||||
|
||||
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||
|
||||
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
||||
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: RenderOptions = {}) {
|
||||
|
||||
options = Object.assign(
|
||||
{
|
||||
@ -116,32 +113,6 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
||||
};
|
||||
}
|
||||
|
||||
async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
|
||||
// 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 && !options.noChildrenList) {
|
||||
await renderChildrenList($renderedContent, note);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
||||
*/
|
||||
@ -163,7 +134,7 @@ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||
}
|
||||
|
||||
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
|
||||
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
|
||||
const encodedTitle = encodeURIComponent(entity.title);
|
||||
|
||||
let url;
|
||||
@ -305,40 +276,6 @@ 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) {
|
||||
let type: string = "";
|
||||
if ("type" in entity) {
|
||||
|
||||
126
apps/client/src/services/content_renderer_text.ts
Normal file
126
apps/client/src/services/content_renderer_text.ts
Normal file
@ -0,0 +1,126 @@
|
||||
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,7 +1,9 @@
|
||||
import clsx from "clsx";
|
||||
import {readCssVar} from "../utils/css-var";
|
||||
import Color, { ColorInstance } from "color";
|
||||
|
||||
const registeredClasses = new Set<string>();
|
||||
const colorsWithHue = new Set<string>();
|
||||
|
||||
// Read the color lightness limits defined in the theme as CSS variables
|
||||
|
||||
@ -26,19 +28,23 @@ function createClassForColor(colorString: string | null) {
|
||||
if (!registeredClasses.has(className)) {
|
||||
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
|
||||
darkThemeColorMinLightness!);
|
||||
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: ${getHue(color) ?? 'unset'};
|
||||
--custom-color-hue: ${hue ?? 'unset'};
|
||||
}
|
||||
</style>`);
|
||||
|
||||
registeredClasses.add(className);
|
||||
if (hue !== undefined) {
|
||||
colorsWithHue.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
return className;
|
||||
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
|
||||
}
|
||||
|
||||
function parseColor(color: string) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import dayjs from "dayjs";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import type { FNoteRow } from "../entities/fnote.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
* @param whether to execute at the beginning (`false`)
|
||||
* @api public
|
||||
*/
|
||||
function debounce<T>(func: (...args: unknown[]) => T, waitMs: number, immediate: boolean = false) {
|
||||
function debounce<T>(func: (...args: any[]) => T, waitMs: number, immediate: boolean = false) {
|
||||
let timeout: any; // TODO: fix once we split client and server.
|
||||
let args: unknown[] | null;
|
||||
let context: unknown;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Modal } from "bootstrap";
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
|
||||
|
||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
||||
if (closeActDialog) {
|
||||
@ -37,8 +38,8 @@ export function closeActiveDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
async function info(message: string) {
|
||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
||||
async function info(message: MessageType, extraProps?: InfoExtraProps) {
|
||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { ...extraProps, message, callback: res }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,7 +13,7 @@ export interface Froca {
|
||||
|
||||
getBlob(entityType: string, entityId: string): Promise<FBlob | null>;
|
||||
getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>;
|
||||
getNoteFromCache(noteId: string): FNote;
|
||||
getNoteFromCache(noteId: string): FNote | undefined;
|
||||
getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[];
|
||||
getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise<FNote[]>;
|
||||
|
||||
|
||||
@ -288,7 +288,7 @@ class FrocaImpl implements Froca {
|
||||
return (await this.getNotes([noteId], silentNotFoundError))[0];
|
||||
}
|
||||
|
||||
getNoteFromCache(noteId: string) {
|
||||
getNoteFromCache(noteId: string): FNote | undefined {
|
||||
if (!noteId) {
|
||||
throw new Error("Empty noteId");
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import shortcutService from "./shortcuts.js";
|
||||
import dialogService from "./dialog.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import { t } from "./i18n.js";
|
||||
import dayjs from "dayjs";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import type NoteContext from "../components/note_context.js";
|
||||
import type Component from "../components/component.js";
|
||||
import { formatLogMessage } from "@triliumnext/commons";
|
||||
@ -77,6 +77,10 @@ export interface Api {
|
||||
|
||||
/**
|
||||
* Entity whose event triggered this execution.
|
||||
*
|
||||
* <p>
|
||||
* For front-end scripts, generally there's no origin entity specified since the scripts are run by the user or automatically by the UI (widgets).
|
||||
* If there is an origin entity specified, then it's going to be a note entity.
|
||||
*/
|
||||
originEntity: unknown | null;
|
||||
|
||||
@ -278,12 +282,16 @@ export interface Api {
|
||||
getActiveContextNote(): FNote;
|
||||
|
||||
/**
|
||||
* @returns returns active context (split)
|
||||
* Obtains the currently active/focused split in the current tab.
|
||||
*
|
||||
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
|
||||
*/
|
||||
getActiveContext(): NoteContext;
|
||||
|
||||
/**
|
||||
* @returns returns active main context (first split in a tab, represents the tab as a whole)
|
||||
* Obtains the main context of the current tab. This is the left-most split.
|
||||
*
|
||||
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
|
||||
*/
|
||||
getActiveMainContext(): NoteContext;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import options from "./options.js";
|
||||
import i18next from "i18next";
|
||||
import i18nextHttpBackend from "i18next-http-backend";
|
||||
import server from "./server.js";
|
||||
import type { Locale } from "@triliumnext/commons";
|
||||
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
let locales: Locale[] | null;
|
||||
@ -13,7 +13,7 @@ let locales: Locale[] | null;
|
||||
export let translationsInitializedPromise = $.Deferred();
|
||||
|
||||
export async function initLocale() {
|
||||
const locale = (options.get("locale") as string) || "en";
|
||||
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
|
||||
|
||||
locales = await server.get<Locale[]>("options/locales");
|
||||
|
||||
@ -27,6 +27,7 @@ export async function initLocale() {
|
||||
returnEmptyString: false
|
||||
});
|
||||
|
||||
await setDayjsLocale(locale);
|
||||
translationsInitializedPromise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import toastService, { type ToastOptions } from "./toast.js";
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
import utils from "./utils.js";
|
||||
@ -57,11 +57,11 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
|
||||
}
|
||||
}
|
||||
|
||||
function makeToast(id: string, message: string): ToastOptions {
|
||||
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
|
||||
return {
|
||||
id: id,
|
||||
id,
|
||||
title: t("import.import-status"),
|
||||
message: message,
|
||||
message,
|
||||
icon: "plus"
|
||||
};
|
||||
}
|
||||
@ -78,7 +78,7 @@ ws.subscribeToMessages(async (message) => {
|
||||
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
|
||||
} else if (message.type === "taskSucceeded") {
|
||||
const toast = makeToast(message.taskId, t("import.successful"));
|
||||
toast.closeAfter = 5000;
|
||||
toast.timeout = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
|
||||
@ -100,7 +100,7 @@ ws.subscribeToMessages(async (message: WebSocketMessage) => {
|
||||
toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount })));
|
||||
} else if (message.type === "taskSucceeded") {
|
||||
const toast = makeToast(message.taskId, t("import.successful"));
|
||||
toast.closeAfter = 5000;
|
||||
toast.timeout = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ async function getActionsForScope(scope: string) {
|
||||
return actions.filter((action) => action.scope === scope);
|
||||
}
|
||||
|
||||
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
|
||||
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component, ntxId: string | null | undefined) {
|
||||
if (!$el[0]) return [];
|
||||
|
||||
const actions = await getActionsForScope(scope);
|
||||
@ -36,7 +36,9 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
|
||||
|
||||
for (const action of actions) {
|
||||
for (const shortcut of action.effectiveShortcuts ?? []) {
|
||||
const binding = shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
|
||||
const binding = shortcutService.bindElShortcut($el, shortcut, () => {
|
||||
component.triggerCommand(action.actionName, { ntxId });
|
||||
});
|
||||
if (binding) {
|
||||
bindings.push(binding);
|
||||
}
|
||||
|
||||
@ -467,28 +467,30 @@ function getReferenceLinkTitleSync(href: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("click", "a", goToLink);
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("auxclick", "a", goToLink); // to handle the middle button
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("contextmenu", "a", linkContextMenu);
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("dblclick", "a", goToLink);
|
||||
if (glob.device !== "print") {
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("click", "a", goToLink);
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("auxclick", "a", goToLink); // to handle the middle button
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("contextmenu", "a", linkContextMenu);
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on("dblclick", "a", goToLink);
|
||||
|
||||
$(document).on("mousedown", "a", (e) => {
|
||||
if (e.which === 2) {
|
||||
// prevent paste on middle click
|
||||
// https://github.com/zadam/trilium/issues/2995
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$(document).on("mousedown", "a", (e) => {
|
||||
if (e.which === 2) {
|
||||
// prevent paste on middle click
|
||||
// https://github.com/zadam/trilium/issues/2995
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
getNotePathFromUrl,
|
||||
|
||||
@ -41,6 +41,17 @@ function parse(value: string) {
|
||||
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 {
|
||||
parse
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import server from "./server.js";
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import toastService from "./toast.js";
|
||||
import type { ToastOptions } from "./toast.js";
|
||||
import type { ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import froca from "./froca.js";
|
||||
@ -97,7 +97,7 @@ async function protectNote(noteId: string, protect: boolean, includingSubtree: b
|
||||
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
|
||||
}
|
||||
|
||||
function makeToast(message: Message, title: string, text: string): ToastOptions {
|
||||
function makeToast(message: Message, title: string, text: string): ToastOptionsWithRequiredId {
|
||||
return {
|
||||
id: message.taskId,
|
||||
title,
|
||||
@ -124,7 +124,7 @@ ws.subscribeToMessages(async (message) => {
|
||||
} else if (message.type === "taskSucceeded") {
|
||||
const text = isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully");
|
||||
const toast = makeToast(message, title, text);
|
||||
toast.closeAfter = 3000;
|
||||
toast.timeout = 3000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
}
|
||||
|
||||
@ -263,7 +263,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
||||
|
||||
const toastService = (await import("./toast.js")).default;
|
||||
|
||||
const messageStr = typeof message === "string" ? message : JSON.stringify(message);
|
||||
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
|
||||
|
||||
if ([400, 404].includes(statusCode) && response && typeof response === "object") {
|
||||
toastService.showError(messageStr);
|
||||
@ -274,10 +274,22 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
||||
...response
|
||||
});
|
||||
} else {
|
||||
const title = `${statusCode} ${method} ${url}`;
|
||||
toastService.showErrorTitleAndMessage(title, messageStr);
|
||||
const { t } = await import("./i18n.js");
|
||||
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
|
||||
toastService.showPersistent({
|
||||
id: "trafik-blocked",
|
||||
icon: "bx bx-unlink",
|
||||
title: t("server.unknown_http_error_title"),
|
||||
message: t("server.traefik_blocks_requests")
|
||||
});
|
||||
} else {
|
||||
toastService.showErrorTitleAndMessage(
|
||||
t("server.unknown_http_error_title"),
|
||||
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
|
||||
15_000);
|
||||
}
|
||||
const { throwError } = await import("./ws.js");
|
||||
throwError(`${title} - ${message}`);
|
||||
throwError(`${statusCode} ${method} ${url} - ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import utils from "./utils.js";
|
||||
|
||||
type ElementType = HTMLElement | Document;
|
||||
type Handler = (e: KeyboardEvent) => void;
|
||||
export type Handler = (e: KeyboardEvent) => void;
|
||||
|
||||
export interface ShortcutBinding {
|
||||
element: HTMLElement | Document;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { signal } from "@preact/signals";
|
||||
|
||||
import utils from "./utils.js";
|
||||
|
||||
export interface ToastOptions {
|
||||
@ -5,112 +7,84 @@ export interface ToastOptions {
|
||||
icon: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
delay?: number;
|
||||
autohide?: boolean;
|
||||
closeAfter?: number;
|
||||
timeout?: number;
|
||||
progress?: number;
|
||||
buttons?: {
|
||||
text: string;
|
||||
onClick: (api: { dismissToast: () => void }) => void;
|
||||
}[];
|
||||
}
|
||||
|
||||
function toast(options: ToastOptions) {
|
||||
const $toast = $(options.title
|
||||
? `\
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">
|
||||
<span class="bx bx-${options.icon}"></span>
|
||||
<span class="toast-title"></span>
|
||||
</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body"></div>
|
||||
</div>`
|
||||
: `
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-icon">
|
||||
<span class="bx bx-${options.icon}"></span>
|
||||
</div>
|
||||
<div class="toast-body"></div>
|
||||
<div class="toast-header">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
export type ToastOptionsWithRequiredId = Omit<ToastOptions, "id"> & Required<Pick<ToastOptions, "id">>;
|
||||
|
||||
$toast.toggleClass("no-title", !options.title);
|
||||
$toast.find(".toast-title").text(options.title ?? "");
|
||||
$toast.find(".toast-body").html(options.message);
|
||||
|
||||
if (options.id) {
|
||||
$toast.attr("id", `toast-${options.id}`);
|
||||
}
|
||||
|
||||
$("#toast-container").append($toast);
|
||||
|
||||
$toast.toast({
|
||||
delay: options.delay || 3000,
|
||||
autohide: !!options.autohide
|
||||
});
|
||||
|
||||
$toast.on("hidden.bs.toast", (e) => e.target.remove());
|
||||
|
||||
$toast.toast("show");
|
||||
|
||||
return $toast;
|
||||
}
|
||||
|
||||
function showPersistent(options: ToastOptions) {
|
||||
let $toast = $(`#toast-${options.id}`);
|
||||
|
||||
if ($toast.length > 0) {
|
||||
$toast.find(".toast-body").html(options.message);
|
||||
function showPersistent(options: ToastOptionsWithRequiredId) {
|
||||
const existingToast = toasts.value.find(toast => toast.id === options.id);
|
||||
if (existingToast) {
|
||||
updateToast(options.id, options);
|
||||
} else {
|
||||
options.autohide = false;
|
||||
|
||||
$toast = toast(options);
|
||||
}
|
||||
|
||||
if (options.closeAfter) {
|
||||
setTimeout(() => $toast.remove(), options.closeAfter);
|
||||
addToast(options);
|
||||
}
|
||||
}
|
||||
|
||||
function closePersistent(id: string) {
|
||||
$(`#toast-${id}`).remove();
|
||||
removeToastFromStore(id);
|
||||
}
|
||||
|
||||
function showMessage(message: string, delay = 2000, icon = "check") {
|
||||
function showMessage(message: string, timeout = 2000, icon = "bx bx-check") {
|
||||
console.debug(utils.now(), "message:", message);
|
||||
|
||||
toast({
|
||||
addToast({
|
||||
icon,
|
||||
message: message,
|
||||
autohide: true,
|
||||
delay
|
||||
message,
|
||||
timeout
|
||||
});
|
||||
}
|
||||
|
||||
export function showError(message: string, delay = 10000) {
|
||||
export function showError(message: string, timeout = 10000) {
|
||||
console.log(utils.now(), "error: ", message);
|
||||
|
||||
toast({
|
||||
icon: "alert",
|
||||
message: message,
|
||||
autohide: true,
|
||||
delay
|
||||
addToast({
|
||||
icon: "bx bx-error-circle",
|
||||
message,
|
||||
timeout
|
||||
});
|
||||
}
|
||||
|
||||
function showErrorTitleAndMessage(title: string, message: string, delay = 10000) {
|
||||
function showErrorTitleAndMessage(title: string, message: string, timeout = 10000) {
|
||||
console.log(utils.now(), "error: ", message);
|
||||
|
||||
toast({
|
||||
title: title,
|
||||
icon: "alert",
|
||||
message: message,
|
||||
autohide: true,
|
||||
delay
|
||||
addToast({
|
||||
title,
|
||||
icon: "bx bx-error-circle",
|
||||
message,
|
||||
timeout
|
||||
});
|
||||
}
|
||||
|
||||
//#region Toast store
|
||||
export const toasts = signal<ToastOptionsWithRequiredId[]>([]);
|
||||
|
||||
function addToast(opts: ToastOptions) {
|
||||
const id = opts.id ?? crypto.randomUUID();
|
||||
const toast = { ...opts, id };
|
||||
toasts.value = [ ...toasts.value, toast ];
|
||||
return id;
|
||||
}
|
||||
|
||||
function updateToast(id: string, partial: Partial<ToastOptions>) {
|
||||
toasts.value = toasts.value.map(toast => {
|
||||
if (toast.id === id) {
|
||||
return { ...toast, ...partial }
|
||||
}
|
||||
return toast;
|
||||
});
|
||||
}
|
||||
|
||||
export function removeToastFromStore(id: string) {
|
||||
toasts.value = toasts.value.filter(toast => toast.id !== id);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
export default {
|
||||
showMessage,
|
||||
showError,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import type { ViewScope } from "./link.js";
|
||||
import FNote from "../entities/fnote";
|
||||
import { snapdom } from "@zumer/snapdom";
|
||||
|
||||
const SVG_MIME = "image/svg+xml";
|
||||
|
||||
@ -150,7 +151,7 @@ export function isMac() {
|
||||
|
||||
export const hasTouchBar = (isMac() && isElectron());
|
||||
|
||||
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
|
||||
export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
|
||||
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
||||
}
|
||||
|
||||
@ -207,7 +208,7 @@ function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function randomString(len: number) {
|
||||
export function randomString(len: number) {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
@ -236,7 +237,7 @@ export function isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function isDesktop() {
|
||||
export function isDesktop() {
|
||||
return (
|
||||
window.glob?.device === "desktop" ||
|
||||
// window.glob.device is not available in setup
|
||||
@ -274,7 +275,7 @@ function getMimeTypeClass(mime: string) {
|
||||
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
||||
}
|
||||
|
||||
function isHtmlEmpty(html: string) {
|
||||
export function isHtmlEmpty(html: string) {
|
||||
if (!html) {
|
||||
return true;
|
||||
} else if (typeof html !== "string") {
|
||||
@ -628,16 +629,69 @@ export function createImageSrcUrl(note: FNote) {
|
||||
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Given a string representation of an SVG, triggers a download of the file on the client device.
|
||||
* Helper function to prepare an element for snapdom rendering.
|
||||
* Handles string parsing and temporary DOM attachment for style computation.
|
||||
*
|
||||
* @param source - Either an SVG/HTML string to be parsed, or an existing SVG/HTML element.
|
||||
* @returns An object containing the prepared element and a cleanup function.
|
||||
* The cleanup function removes temporarily attached elements from the DOM,
|
||||
* or is a no-op if the element was already in the DOM.
|
||||
*/
|
||||
function prepareElementForSnapdom(source: string | SVGElement | HTMLElement): {
|
||||
element: SVGElement | HTMLElement;
|
||||
cleanup: () => void;
|
||||
} {
|
||||
if (typeof source === 'string') {
|
||||
const parser = new DOMParser();
|
||||
|
||||
// Detect if content is SVG or HTML
|
||||
const isSvg = source.trim().startsWith('<svg');
|
||||
const mimeType = isSvg ? SVG_MIME : 'text/html';
|
||||
|
||||
const doc = parser.parseFromString(source, mimeType);
|
||||
const element = doc.documentElement;
|
||||
|
||||
// Temporarily attach to DOM for proper style computation
|
||||
element.style.position = 'absolute';
|
||||
element.style.left = '-9999px';
|
||||
element.style.top = '-9999px';
|
||||
document.body.appendChild(element);
|
||||
|
||||
return {
|
||||
element,
|
||||
cleanup: () => document.body.removeChild(element)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
element: source,
|
||||
cleanup: () => {} // No-op for existing elements
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an SVG using snapdom for proper rendering. Can accept either an SVG string, an SVG element, or an HTML element.
|
||||
*
|
||||
* @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
|
||||
* @param svgContent the content of the SVG file download.
|
||||
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be downloaded.
|
||||
*/
|
||||
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
||||
const filename = `${nameWithoutExtension}.svg`;
|
||||
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
||||
triggerDownload(filename, dataUrl);
|
||||
async function downloadAsSvg(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
|
||||
const { element, cleanup } = prepareElementForSnapdom(svgSource);
|
||||
|
||||
try {
|
||||
const result = await snapdom(element, {
|
||||
backgroundColor: "transparent",
|
||||
scale: 2
|
||||
});
|
||||
triggerDownload(`${nameWithoutExtension}.svg`, result.url);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -658,62 +712,26 @@ function triggerDownload(fileName: string, dataUrl: string) {
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device.
|
||||
*
|
||||
* Note that the SVG must specify its width and height as attributes in order for it to be rendered.
|
||||
* Downloads an SVG as PNG using snapdom. Can accept either an SVG string, an SVG element, or an HTML element.
|
||||
*
|
||||
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
|
||||
* @param svgContent the content of the SVG file download.
|
||||
* @returns a promise which resolves if the operation was successful, or rejects if it failed (permissions issue or some other issue).
|
||||
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be converted to PNG.
|
||||
*/
|
||||
function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// First, we need to determine the width and the height from the input SVG.
|
||||
const result = getSizeFromSvg(svgContent);
|
||||
if (!result) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
async function downloadAsPng(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
|
||||
const { element, cleanup } = prepareElementForSnapdom(svgSource);
|
||||
|
||||
// Convert the image to a blob.
|
||||
const { width, height } = result;
|
||||
|
||||
// Create an image element and load the SVG.
|
||||
const imageEl = new Image();
|
||||
imageEl.width = width;
|
||||
imageEl.height = height;
|
||||
imageEl.crossOrigin = "anonymous";
|
||||
imageEl.onload = () => {
|
||||
try {
|
||||
// Draw the image with a canvas.
|
||||
const canvasEl = document.createElement("canvas");
|
||||
canvasEl.width = imageEl.width;
|
||||
canvasEl.height = imageEl.height;
|
||||
document.body.appendChild(canvasEl);
|
||||
|
||||
const ctx = canvasEl.getContext("2d");
|
||||
if (!ctx) {
|
||||
reject();
|
||||
}
|
||||
|
||||
ctx?.drawImage(imageEl, 0, 0);
|
||||
|
||||
const imgUri = canvasEl.toDataURL("image/png")
|
||||
triggerDownload(`${nameWithoutExtension}.png`, imgUri);
|
||||
document.body.removeChild(canvasEl);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
reject();
|
||||
}
|
||||
};
|
||||
imageEl.onerror = (e) => reject(e);
|
||||
imageEl.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
||||
});
|
||||
try {
|
||||
const result = await snapdom(element, {
|
||||
backgroundColor: "transparent",
|
||||
scale: 2
|
||||
});
|
||||
const pngImg = await result.toPng();
|
||||
await triggerDownload(`${nameWithoutExtension}.png`, pngImg.src);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
export function getSizeFromSvg(svgContent: string) {
|
||||
const svgDocument = (new DOMParser()).parseFromString(svgContent, SVG_MIME);
|
||||
|
||||
@ -925,8 +943,8 @@ export default {
|
||||
areObjectsEqual,
|
||||
copyHtmlToClipboard,
|
||||
createImageSrcUrl,
|
||||
downloadSvg,
|
||||
downloadSvgAsPng,
|
||||
downloadAsSvg,
|
||||
downloadAsPng,
|
||||
compareVersions,
|
||||
isUpdateAvailable,
|
||||
isLaunchBarConfig
|
||||
|
||||
@ -25,7 +25,11 @@
|
||||
--bs-body-font-weight: var(--main-font-weight) !important;
|
||||
--bs-body-color: var(--main-text-color) !important;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--tn-modal-max-height: 90vh;
|
||||
|
||||
--tree-item-light-theme-max-color-lightness: 50;
|
||||
--tree-item-dark-theme-min-color-lightness: 75;
|
||||
}
|
||||
|
||||
body#trilium-app.motion-disabled *,
|
||||
@ -212,6 +216,16 @@ input::placeholder,
|
||||
background-color: var(--modal-backdrop-color) !important;
|
||||
}
|
||||
|
||||
body.mobile .modal .modal-dialog {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.mobile .modal .modal-content {
|
||||
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.component {
|
||||
contain: size;
|
||||
}
|
||||
@ -243,6 +257,11 @@ button.close:hover {
|
||||
color: var(--hover-item-text-color);
|
||||
}
|
||||
|
||||
button.custom-title-bar-button {
|
||||
background: transparent;
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--modal-background-color) !important;
|
||||
}
|
||||
@ -439,7 +458,8 @@ body.desktop .tabulator-popup-container,
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||
body #context-menu-container .dropdown-item > span {
|
||||
body #context-menu-container .dropdown-item > span,
|
||||
body.mobile .dropdown .dropdown-submenu > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@ -494,6 +514,10 @@ body #context-menu-container .dropdown-item > span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-menu .note-color-picker {
|
||||
padding: 4px 12px 8px 12px;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
outline: none !important;
|
||||
@ -576,11 +600,6 @@ button.btn-sm {
|
||||
color: var(--left-pane-text-color);
|
||||
}
|
||||
|
||||
.btn.active:not(.btn-primary) {
|
||||
background-color: var(--button-disabled-background-color) !important;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.ck.ck-block-toolbar-button {
|
||||
transform: translateX(7px);
|
||||
color: var(--muted-text-color);
|
||||
@ -701,11 +720,6 @@ table.promoted-attributes-in-tooltip th {
|
||||
z-index: 32767 !important;
|
||||
}
|
||||
|
||||
.tooltip-trigger {
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bs-tooltip-bottom .tooltip-arrow::before {
|
||||
border-bottom-color: var(--main-border-color) !important;
|
||||
}
|
||||
@ -1001,7 +1015,7 @@ div[data-notify="container"] {
|
||||
font-family: var(--monospace-font-family);
|
||||
}
|
||||
|
||||
svg.ck-icon .note-icon {
|
||||
svg.ck-icon.note-icon {
|
||||
color: var(--main-text-color);
|
||||
font-size: 20px;
|
||||
}
|
||||
@ -1112,10 +1126,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note-detail-empty {
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
|
||||
}
|
||||
@ -1125,50 +1135,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
#toast-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 20px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
--bs-toast-bg: var(--accented-background-color);
|
||||
--bs-toast-color: var(--main-text-color);
|
||||
z-index: 9999999999 !important;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
background-color: var(--more-accented-background-color) !important;
|
||||
color: var(--main-text-color) !important;
|
||||
}
|
||||
|
||||
.toast-body {
|
||||
white-space: preserve-breaks;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toast.no-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.toast.no-title .toast-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
|
||||
}
|
||||
|
||||
.toast.no-title .toast-body {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.toast.no-title .toast-header {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.ck-mentions .ck-button {
|
||||
font-size: var(--detail-font-size) !important;
|
||||
padding: 5px;
|
||||
@ -1300,11 +1266,11 @@ body.mobile #context-menu-container.mobile-bottom-menu {
|
||||
inset-inline-end: 0 !important;
|
||||
bottom: 0 !important;
|
||||
top: unset !important;
|
||||
max-height: 70vh;
|
||||
max-height: var(--tn-modal-max-height);
|
||||
overflow: auto !important;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
padding-bottom: env(safe-area-inset-bottom) !important;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), var(--padding, var(--menu-padding-size))) !important;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu {
|
||||
@ -1363,6 +1329,20 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.right-dropdown-widget .right-dropdown-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-trigger {
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#launcher-pane.horizontal .right-dropdown-widget {
|
||||
width: 53px;
|
||||
}
|
||||
@ -1538,12 +1518,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
@media (max-width: 991px) {
|
||||
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
|
||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
||||
--dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
|
||||
position: fixed !important;
|
||||
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)) !important;
|
||||
bottom: var(--dropdown-bottom) !important;
|
||||
top: unset !important;
|
||||
inset-inline-start: 0 !important;
|
||||
inset-inline-end: 0 !important;
|
||||
transform: unset !important;
|
||||
overflow-y: auto;
|
||||
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
|
||||
}
|
||||
|
||||
#mobile-sidebar-container {
|
||||
@ -1578,6 +1561,14 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-split.empty-note {
|
||||
--max-content-width: var(--preferred-max-content-width);
|
||||
}
|
||||
|
||||
.note-detail-empty {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
#mobile-sidebar-container.show #mobile-sidebar-wrapper {
|
||||
transform: translateX(0);
|
||||
}
|
||||
@ -1640,46 +1631,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
body.mobile .modal-dialog.modal-dialog-scrollable {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .modal-dialog {
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .modal-body {
|
||||
height: 100% !important;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .revision-list {
|
||||
height: unset;
|
||||
max-height: 20vh;
|
||||
border-bottom: 1px solid var(--main-border-color) !important;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .modal-body > .revision-content-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .modal-body > .revision-content-wrapper > div:first-of-type {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .revision-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .revision-title-buttons {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body.mobile .revisions-dialog .revision-content {
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile, tablet mode */
|
||||
@ -1985,7 +1936,7 @@ body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
body.electron.platform-darwin:not(.native-titlebar) #tab-row-left-spacer {
|
||||
body.electron.platform-darwin:not(.native-titlebar):not(.full-screen) #tab-row-left-spacer {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@ -2434,6 +2385,15 @@ footer.webview-footer button {
|
||||
.admonition.caution::before { content: "\eac7"; }
|
||||
.admonition.warning::before { content: "\eac5"; }
|
||||
|
||||
.ck-content ul.todo-list li span.todo-list__label__description {
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.ck-content ul.todo-list li:has(> span.todo-list__label input[type="checkbox"]:checked) > span.todo-list__label span.todo-list__label__description {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.chat-options-container {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
@ -2559,9 +2519,38 @@ iframe.print-iframe {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scrolling-container > .note-detail.full-height,
|
||||
.note-detail.full-height,
|
||||
.scrolling-container > .note-list-widget.full-height {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Calendar collection */
|
||||
|
||||
.calendar-view a.fc-timegrid-event,
|
||||
.calendar-view a.fc-daygrid-event {
|
||||
/* Workaround: set font weight only if the theme-next is not active */
|
||||
font-weight: var(--root-background, 800);
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
body.mobile {
|
||||
.split-note-container-widget {
|
||||
flex-direction: column !important;
|
||||
|
||||
.note-split {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-split.visible + .note-split.visible {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
#root-widget.virtual-keyboard-opened .note-split:not(:focus-within) {
|
||||
max-height: 80px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,9 @@
|
||||
|
||||
--mermaid-theme: dark;
|
||||
--native-titlebar-background: #00000000;
|
||||
|
||||
--calendar-coll-event-background-saturation: 30%;
|
||||
--calendar-coll-event-background-lightness: 30%;
|
||||
}
|
||||
|
||||
body ::-webkit-calendar-picker-indicator {
|
||||
@ -109,3 +112,6 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
.use-note-color {
|
||||
--custom-color: var(--dark-theme-custom-color);
|
||||
}
|
||||
@ -80,6 +80,9 @@ html {
|
||||
|
||||
--mermaid-theme: default;
|
||||
--native-titlebar-background: #ffffff00;
|
||||
|
||||
--calendar-coll-event-background-lightness: 95%;
|
||||
--calendar-coll-event-background-saturation: 80%;
|
||||
}
|
||||
|
||||
#left-pane .fancytree-node.tinted {
|
||||
@ -91,4 +94,8 @@ html {
|
||||
.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);
|
||||
}
|
||||
@ -41,6 +41,9 @@
|
||||
--cmd-button-keyboard-shortcut-color: white;
|
||||
--cmd-button-disabled-opacity: 0.5;
|
||||
|
||||
--button-group-active-button-background: #ffffff4e;
|
||||
--button-group-active-button-text-color: white;
|
||||
|
||||
--icon-button-color: currentColor;
|
||||
--icon-button-hover-background: var(--hover-item-background-color);
|
||||
--icon-button-hover-color: var(--hover-item-text-color);
|
||||
@ -98,6 +101,7 @@
|
||||
--menu-item-delimiter-color: #ffffff1c;
|
||||
--menu-item-group-header-color: #ffffff91;
|
||||
--menu-section-background-color: #fefefe08;
|
||||
--menu-submenu-mobile-background-color: rgba(0, 0, 0, 0.15);
|
||||
|
||||
--modal-backdrop-color: #000;
|
||||
--modal-shadow-color: rgba(0, 0, 0, .5);
|
||||
@ -266,6 +270,14 @@
|
||||
--ck-editor-toolbar-button-on-color: white;
|
||||
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
|
||||
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
|
||||
|
||||
--calendar-coll-event-background-saturation: 25%;
|
||||
--calendar-coll-event-background-lightness: 20%;
|
||||
--calendar-coll-event-background-color: #3c3c3c;
|
||||
--calendar-coll-event-text-color: white;
|
||||
--calendar-coll-event-hover-filter: brightness(1.25);
|
||||
--callendar-coll-event-archived-sripe-color: #00000026;
|
||||
--calendar-coll-today-background-color: #ffffff08;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -300,8 +312,12 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
border-color: var(--muted-text-color) !important;
|
||||
}
|
||||
|
||||
.tinted-quick-edit-dialog {
|
||||
.quick-edit-dialog-wrapper.with-hue {
|
||||
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
|
||||
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
|
||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
|
||||
}
|
||||
|
||||
.use-note-color {
|
||||
--custom-color: var(--dark-theme-custom-color);
|
||||
}
|
||||
@ -41,6 +41,9 @@
|
||||
--cmd-button-keyboard-shortcut-color: black;
|
||||
--cmd-button-disabled-opacity: 0.5;
|
||||
|
||||
--button-group-active-button-background: #00000026;
|
||||
--button-group-active-button-text-color: black;
|
||||
|
||||
--icon-button-color: currentColor;
|
||||
--icon-button-hover-background: var(--hover-item-background-color);
|
||||
--icon-button-hover-color: var(--hover-item-text-color);
|
||||
@ -265,6 +268,14 @@
|
||||
--ck-editor-toolbar-button-on-color: black;
|
||||
--ck-editor-toolbar-button-on-shadow: none;
|
||||
--ck-editor-toolbar-dropdown-button-open-background: #0000000f;
|
||||
|
||||
--calendar-coll-event-background-lightness: 95%;
|
||||
--calendar-coll-event-background-saturation: 80%;
|
||||
--calendar-coll-event-background-color: #eaeaea;
|
||||
--calendar-coll-event-text-color: black;
|
||||
--calendar-coll-event-hover-filter: brightness(.95) saturate(1.25);
|
||||
--callendar-coll-event-archived-sripe-color: #0000000a;
|
||||
--calendar-coll-today-background-color: #00000006;
|
||||
}
|
||||
|
||||
#left-pane .fancytree-node.tinted {
|
||||
@ -276,7 +287,7 @@
|
||||
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
|
||||
}
|
||||
|
||||
.tinted-quick-edit-dialog {
|
||||
.quick-edit-dialog-wrapper.with-hue {
|
||||
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
|
||||
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
|
||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
|
||||
--menu-padding-size: 8px;
|
||||
--menu-item-icon-vert-offset: -2px;
|
||||
--menu-submenu-mobile-background-color: rgba(255, 255, 255, 0.15);
|
||||
|
||||
--more-accented-background-color: var(--card-background-hover-color);
|
||||
|
||||
@ -99,6 +100,14 @@
|
||||
--tree-item-dark-theme-min-color-lightness: 65;
|
||||
}
|
||||
|
||||
body {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.selectable-text {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
body.backdrop-effects-disabled {
|
||||
/* Backdrop effects are disabled, replace the menu background color with the
|
||||
* no-backdrop fallback color */
|
||||
@ -119,17 +128,6 @@ body.backdrop-effects-disabled {
|
||||
font-size: 0.9rem !important;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu {
|
||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||
border-radius: var(--dropdown-border-radius);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu .dropdown-menu {
|
||||
backdrop-filter: unset !important;
|
||||
border-radius: unset !important;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu::before,
|
||||
:root .ck.ck-dropdown__panel::before,
|
||||
:root .excalidraw .popover::before,
|
||||
@ -157,17 +155,12 @@ body.desktop .dropdown-submenu .dropdown-menu::before {
|
||||
content: unset;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-submenu .dropdown-menu {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-submenu .dropdown-menu {
|
||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dropdown-item,
|
||||
body.mobile .dropdown-submenu .dropdown-toggle,
|
||||
.excalidraw .context-menu .context-menu-item {
|
||||
--menu-item-start-padding: 8px;
|
||||
--menu-item-end-padding: 22px;
|
||||
@ -201,10 +194,6 @@ body.mobile .dropdown-item:not(:last-of-type) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-submenu:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
html body .dropdown-item.disabled,
|
||||
html body .dropdown-item[disabled] {
|
||||
color: var(--menu-text-color) !important;
|
||||
@ -321,17 +310,126 @@ body.desktop .dropdown-menu.static .dropdown-item.active {
|
||||
--active-item-text-color: var(--menu-text-color);
|
||||
}
|
||||
|
||||
/* #region Mobile tweaks for dropdown menus */
|
||||
body.mobile #context-menu-cover {
|
||||
transition: background-color 150ms ease-in;
|
||||
|
||||
&.show {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
&.global-menu-cover {
|
||||
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
|
||||
|
||||
@media (min-width: 992px) {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu.mobile-bottom-menu,
|
||||
body.mobile .dropdown.global-menu .dropdown-menu {
|
||||
border-radius: var(--dropdown-border-radius) var(--dropdown-border-radius) 0 0;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu {
|
||||
--dropdown-menu-padding-vertical: 0.7em;
|
||||
--dropdown-menu-padding-horizontal: 1em;
|
||||
--hover-item-background-color: var(--card-background-color);
|
||||
font-size: 1em !important;
|
||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||
position: relative;
|
||||
|
||||
.dropdown-toggle::after {
|
||||
top: 0.5em;
|
||||
right: var(--dropdown-menu-padding-horizontal);
|
||||
transform: translateX(50%) rotate(90deg);
|
||||
}
|
||||
|
||||
.dropdown-item.submenu-open .dropdown-toggle::after {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.dropdown-item,
|
||||
.dropdown-custom-item {
|
||||
margin-bottom: 0;
|
||||
padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal) !important;
|
||||
background: var(--card-background-color);
|
||||
border-bottom: 1px solid var(--menu-item-delimiter-color) !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.dropdown-item:first-of-type,
|
||||
.dropdown-divider + .dropdown-item,
|
||||
.dropdown-custom-item:first-of-type,
|
||||
.dropdown-divider + .dropdown-custom-item {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.dropdown-item:last-of-type,
|
||||
.dropdown-item:has(+ .dropdown-divider),
|
||||
.dropdown-custom-item:last-of-type,
|
||||
.dropdown-custom-item:has(+ .dropdown-divider) {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dropdown-submenu {
|
||||
padding: 0 !important;
|
||||
backdrop-filter: unset !important;
|
||||
|
||||
.dropdown-toggle {
|
||||
padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
--menu-background-color: --menu-submenu-mobile-background-color;
|
||||
--bs-dropdown-divider-margin-y: 0.25rem;
|
||||
border-radius: 0;
|
||||
max-height: 0;
|
||||
transition: max-height 100ms ease-in;
|
||||
display: block !important;
|
||||
|
||||
&.show {
|
||||
max-height: 1000px;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.submenu-open {
|
||||
.dropdown-toggle {
|
||||
padding-bottom: var(--dropdown-menu-padding-vertical);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-custom-item:has(.note-color-picker) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.note-color-picker {
|
||||
padding: 0;
|
||||
width: fit-content;
|
||||
|
||||
.color-cell {
|
||||
--color-picker-cell-size: 26px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
body.desktop .dropdown-menu .dropdown-toggle::after {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu .dropdown-toggle::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
/* Dropdown item button (used in zoom buttons in global menu) */
|
||||
|
||||
@ -347,6 +445,12 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
outline: 2px solid var(--input-focus-outline-color) !important;
|
||||
}
|
||||
|
||||
:root .dropdown-menu .note-color-picker {
|
||||
padding: 4px 10px;
|
||||
--note-color-picker-clear-color-cell-background: var(--main-text-color);
|
||||
--note-color-picker-clear-color-cell-selection-outline-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* TOASTS
|
||||
*/
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
|
||||
.modal .modal-header .btn-close,
|
||||
.modal .modal-header .help-button,
|
||||
.modal .modal-header .custom-title-bar-button,
|
||||
#toast-container .toast .toast-header .btn-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -55,15 +56,17 @@
|
||||
font-family: boxicons;
|
||||
}
|
||||
|
||||
.modal .modal-header .help-button {
|
||||
.modal .modal-header .help-button,
|
||||
.modal .modal-header .custom-title-bar-button {
|
||||
margin-inline-end: 0;
|
||||
font-size: calc(var(--modal-control-button-size) * .75);
|
||||
font-size: calc(var(--modal-control-button-size) * .70);
|
||||
font-family: unset;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal .modal-header .btn-close:hover,
|
||||
.modal .modal-header .help-button:hover,
|
||||
.modal .modal-header .custom-title-bar-button:hover,
|
||||
#toast-container .toast .toast-header .btn-close:hover {
|
||||
background: var(--modal-control-button-hover-background);
|
||||
color: var(--modal-control-button-hover-color);
|
||||
@ -71,6 +74,7 @@
|
||||
|
||||
.modal .modal-header .btn-close:active,
|
||||
.modal .modal-header .help-button:active,
|
||||
.modal .modal-header .custom-title-bar-button:active,
|
||||
#toast-container .toast .toast-header .btn-close:active {
|
||||
transform: scale(.85);
|
||||
}
|
||||
|
||||
@ -17,6 +17,10 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .c
|
||||
padding: 4px 16px;
|
||||
background: var(--cmd-button-background-color);
|
||||
color: var(--cmd-button-text-color);
|
||||
|
||||
&.dropdown-toggle-split {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
button.btn.btn-primary:hover,
|
||||
@ -142,6 +146,14 @@ button.btn.btn-success kbd {
|
||||
outline: 2px solid var(--input-focus-outline-color);
|
||||
}
|
||||
|
||||
/* Button groups */
|
||||
|
||||
/* Active button */
|
||||
:root .btn-group button.btn.active {
|
||||
background-color: var(--button-group-active-button-background);
|
||||
color: var(--button-group-active-button-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* Input boxes
|
||||
*/
|
||||
|
||||
@ -526,11 +526,14 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck
|
||||
.ck-mermaid__editing-view {
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--code-block-box-shadow);
|
||||
padding: 0;
|
||||
box-shadow: var(--code-block-box-shadow);
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
|
||||
:root .ck-content pre:has(> code) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root .ck-content pre {
|
||||
--icon-button-size: 1.8em;
|
||||
--copy-button-width: var(--icon-button-size);
|
||||
@ -643,7 +646,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
|
||||
}
|
||||
}
|
||||
|
||||
.note-detail-printable:not(.word-wrap) pre code {
|
||||
.ck-content:not(.word-wrap) pre code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
|
||||
@ -124,12 +124,8 @@
|
||||
|
||||
/* The container */
|
||||
|
||||
.note-split.empty-note {
|
||||
--max-content-width: 70%;
|
||||
}
|
||||
|
||||
.note-split.empty-note div.note-detail {
|
||||
margin: 50px auto;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* The search results list */
|
||||
|
||||
@ -212,7 +212,8 @@ body[dir=ltr] #launcher-container {
|
||||
}
|
||||
|
||||
#launcher-pane .launcher-button,
|
||||
#launcher-pane .dropdown {
|
||||
#launcher-pane .right-dropdown-widget,
|
||||
#launcher-pane .global-menu {
|
||||
width: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
||||
height: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
||||
margin: var(--launcher-pane-button-gap) var(--launcher-pane-button-margin);
|
||||
@ -345,7 +346,7 @@ body[dir=ltr] #launcher-container {
|
||||
*/
|
||||
|
||||
.calendar-dropdown-widget {
|
||||
padding: 12px;
|
||||
padding: 18px;
|
||||
color: var(--calendar-color);
|
||||
user-select: none;
|
||||
}
|
||||
@ -1428,9 +1429,7 @@ div.promoted-attribute-cell .tn-checkbox {
|
||||
height: 1cap;
|
||||
}
|
||||
|
||||
/* Relocate the checkbox before the label */
|
||||
div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
|
||||
order: -1;
|
||||
margin-inline-end: 1.5em;
|
||||
}
|
||||
|
||||
@ -1450,12 +1449,20 @@ div.promoted-attribute-cell .multiplicity:has(span) span {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell.promoted-attribute-label-color {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell.promoted-attribute-label-color .input-group {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Floating buttons
|
||||
*/
|
||||
|
||||
/* Floating buttons container */
|
||||
div#center-pane .floating-buttons-children {
|
||||
.floating-buttons-children {
|
||||
opacity: 1;
|
||||
min-height: var(--floating-button-height);
|
||||
transform-origin: right;
|
||||
@ -1467,12 +1474,12 @@ div#center-pane .floating-buttons-children {
|
||||
opacity 250ms ease-out;
|
||||
}
|
||||
|
||||
body[dir=rtl] div#center-pane .floating-buttons-children {
|
||||
body[dir=rtl] .floating-buttons-children {
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
/* Floating buttons container (collapsed) */
|
||||
div#center-pane .floating-buttons-children.temporarily-hidden {
|
||||
.floating-buttons-children.temporarily-hidden {
|
||||
display: flex !important;
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
|
||||
@ -87,7 +87,11 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
let position = 0;
|
||||
for (const [ key, value ] of Object.entries(noteDef)) {
|
||||
const attributeId = utils.randomString(12);
|
||||
const name = key.substring(1);
|
||||
let name = key.substring(1);
|
||||
const isInheritable = key.endsWith("(inheritable)");
|
||||
if (isInheritable) {
|
||||
name = name.substring(0, name.length - "(inheritable)".length);
|
||||
}
|
||||
|
||||
let attribute: FAttribute | null = null;
|
||||
if (key.startsWith("#")) {
|
||||
@ -98,7 +102,7 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
name,
|
||||
value,
|
||||
position,
|
||||
isInheritable: false
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
@ -110,7 +114,7 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
name,
|
||||
value,
|
||||
position,
|
||||
isInheritable: false
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,8 @@ function mockServer() {
|
||||
attributes: []
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`Unsupported GET to mocked server: ${url}`);
|
||||
},
|
||||
|
||||
async post(url: string, data: object) {
|
||||
|
||||
@ -201,8 +201,12 @@
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"relation": "العلاقة",
|
||||
"backlink": "{{count}} رابط راجع",
|
||||
"backlinks": "{{count}} روابط راجعة"
|
||||
"backlink_zero": "",
|
||||
"backlink_one": "{{count}} رابط راجع",
|
||||
"backlink_two": "",
|
||||
"backlink_few": "",
|
||||
"backlink_many": "{{count}} روابط راجعة",
|
||||
"backlink_other": ""
|
||||
},
|
||||
"note_icon": {
|
||||
"category": "الفئة:",
|
||||
@ -230,7 +234,6 @@
|
||||
"geo-map": "الخريطة الجغرافية",
|
||||
"collapse_all_notes": "طي كل الملاحظات",
|
||||
"include_archived_notes": "عرض الملاحظات المؤرشفة",
|
||||
"expand_all_children": "توسيع جميع العناصر الفرعية",
|
||||
"presentation": "عرض تقديمي",
|
||||
"invalid_view_type": "نوع العرض {{type}} غير صالح"
|
||||
},
|
||||
@ -945,7 +948,7 @@
|
||||
"move-to-available-launchers": "نقل الى المشغلات المتوفرة",
|
||||
"duplicate-launcher": "تكرار المشغل <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"editable_text": {
|
||||
"auto-detect-language": "تم اكتشافه تلقائيا"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
|
||||
@ -1,185 +1,187 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Sobre Trilium Notes",
|
||||
"homepage": "Pàgina principal:"
|
||||
},
|
||||
"add_link": {
|
||||
"note": "Nota"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"prefix": "Prefix: ",
|
||||
"save": "Desa"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"labels": "Etiquetes",
|
||||
"relations": "Relacions",
|
||||
"notes": "Notes",
|
||||
"other": "Altres"
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "Confirmació",
|
||||
"cancel": "Cancel·la",
|
||||
"ok": "OK"
|
||||
},
|
||||
"delete_notes": {
|
||||
"close": "Tanca",
|
||||
"cancel": "Cancel·la",
|
||||
"ok": "OK"
|
||||
},
|
||||
"export": {
|
||||
"close": "Tanca",
|
||||
"export": "Exporta"
|
||||
},
|
||||
"help": {
|
||||
"troubleshooting": "Solució de problemes",
|
||||
"other": "Altres"
|
||||
},
|
||||
"import": {
|
||||
"options": "Opcions",
|
||||
"import": "Importa"
|
||||
},
|
||||
"include_note": {
|
||||
"label_note": "Nota"
|
||||
},
|
||||
"info": {
|
||||
"closeButton": "Tanca",
|
||||
"okButton": "OK"
|
||||
},
|
||||
"note_type_chooser": {
|
||||
"templates": "Plantilles:"
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Sol·licitud",
|
||||
"defaultTitle": "Sol·licitud"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Tanca"
|
||||
},
|
||||
"recent_changes": {
|
||||
"undelete_link": "recuperar"
|
||||
},
|
||||
"revisions": {
|
||||
"restore_button": "Restaura",
|
||||
"delete_button": "Suprimeix",
|
||||
"download_button": "Descarrega",
|
||||
"mime": "MIME: ",
|
||||
"preview": "Vista prèvia:"
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"title": "títol",
|
||||
"ascending": "ascendent",
|
||||
"descending": "descendent",
|
||||
"folders": "Carpetes"
|
||||
},
|
||||
"upload_attachments": {
|
||||
"options": "Opcions",
|
||||
"upload": "Puja"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"name": "Nom",
|
||||
"value": "Valor",
|
||||
"promoted": "Destacat",
|
||||
"promoted_alias": "Àlies",
|
||||
"multiplicity": "Multiplicitat",
|
||||
"label_type": "Tipus",
|
||||
"text": "Text",
|
||||
"number": "Número",
|
||||
"boolean": "Booleà",
|
||||
"date": "Data",
|
||||
"time": "Hora",
|
||||
"url": "URL",
|
||||
"precision": "Precisió",
|
||||
"digits": "dígits",
|
||||
"inheritable": "Heretable",
|
||||
"delete": "Suprimeix",
|
||||
"color_type": "Color"
|
||||
},
|
||||
"rename_label": {
|
||||
"to": "Per"
|
||||
},
|
||||
"move_note": {
|
||||
"to": "a"
|
||||
},
|
||||
"add_relation": {
|
||||
"to": "a"
|
||||
},
|
||||
"rename_relation": {
|
||||
"to": "Per"
|
||||
},
|
||||
"update_relation_target": {
|
||||
"to": "a"
|
||||
},
|
||||
"attachments_actions": {
|
||||
"download": "Descarrega"
|
||||
},
|
||||
"calendar": {
|
||||
"mon": "Dl",
|
||||
"tue": "Dt",
|
||||
"wed": "dc",
|
||||
"thu": "Dj",
|
||||
"fri": "Dv",
|
||||
"sat": "Ds",
|
||||
"sun": "Dg",
|
||||
"january": "Gener",
|
||||
"february": "Febrer",
|
||||
"march": "Març",
|
||||
"april": "Abril",
|
||||
"may": "Maig",
|
||||
"june": "Juny",
|
||||
"july": "Juliol",
|
||||
"august": "Agost",
|
||||
"september": "Setembre",
|
||||
"october": "Octubre",
|
||||
"november": "Novembre",
|
||||
"december": "Desembre"
|
||||
},
|
||||
"global_menu": {
|
||||
"menu": "Menú",
|
||||
"options": "Opcions",
|
||||
"zoom": "Zoom",
|
||||
"advanced": "Avançat",
|
||||
"logout": "Tanca la sessió"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"relation": "relació"
|
||||
},
|
||||
"note_icon": {
|
||||
"category": "Categoria:",
|
||||
"search": "Cerca:"
|
||||
},
|
||||
"basic_properties": {
|
||||
"editable": "Editable",
|
||||
"language": "Llengua"
|
||||
},
|
||||
"book_properties": {
|
||||
"grid": "Graella",
|
||||
"list": "Llista",
|
||||
"collapse": "Replega",
|
||||
"expand": "Desplega",
|
||||
"calendar": "Calendari",
|
||||
"table": "Taula",
|
||||
"board": "Tauler"
|
||||
},
|
||||
"edited_notes": {
|
||||
"deleted": "(suprimit)"
|
||||
},
|
||||
"file_properties": {
|
||||
"download": "Descarrega",
|
||||
"open": "Obre",
|
||||
"title": "Fitxer"
|
||||
},
|
||||
"image_properties": {
|
||||
"download": "Descarrega",
|
||||
"open": "Obre",
|
||||
"title": "Imatge"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"created": "Creat",
|
||||
"modified": "Modificat",
|
||||
"type": "Tipus",
|
||||
"calculate": "calcula"
|
||||
},
|
||||
"note_paths": {
|
||||
"archived": "Arxivat"
|
||||
}
|
||||
"about": {
|
||||
"title": "Sobre Trilium Notes",
|
||||
"homepage": "Pàgina principal:",
|
||||
"app_version": "Versió de l'aplicació:",
|
||||
"db_version": "Versió de la base de dades:"
|
||||
},
|
||||
"add_link": {
|
||||
"note": "Nota"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"prefix": "Prefix: ",
|
||||
"save": "Desa"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"labels": "Etiquetes",
|
||||
"relations": "Relacions",
|
||||
"notes": "Notes",
|
||||
"other": "Altres"
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "Confirmació",
|
||||
"cancel": "Cancel·la",
|
||||
"ok": "OK"
|
||||
},
|
||||
"delete_notes": {
|
||||
"close": "Tanca",
|
||||
"cancel": "Cancel·la",
|
||||
"ok": "OK"
|
||||
},
|
||||
"export": {
|
||||
"close": "Tanca",
|
||||
"export": "Exporta"
|
||||
},
|
||||
"help": {
|
||||
"troubleshooting": "Solució de problemes",
|
||||
"other": "Altres"
|
||||
},
|
||||
"import": {
|
||||
"options": "Opcions",
|
||||
"import": "Importa"
|
||||
},
|
||||
"include_note": {
|
||||
"label_note": "Nota"
|
||||
},
|
||||
"info": {
|
||||
"closeButton": "Tanca",
|
||||
"okButton": "OK"
|
||||
},
|
||||
"note_type_chooser": {
|
||||
"templates": "Plantilles:"
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Sol·licitud",
|
||||
"defaultTitle": "Sol·licitud"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Tanca"
|
||||
},
|
||||
"recent_changes": {
|
||||
"undelete_link": "recuperar"
|
||||
},
|
||||
"revisions": {
|
||||
"restore_button": "Restaura",
|
||||
"delete_button": "Suprimeix",
|
||||
"download_button": "Descarrega",
|
||||
"mime": "MIME: ",
|
||||
"preview": "Vista prèvia:"
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"title": "títol",
|
||||
"ascending": "ascendent",
|
||||
"descending": "descendent",
|
||||
"folders": "Carpetes"
|
||||
},
|
||||
"upload_attachments": {
|
||||
"options": "Opcions",
|
||||
"upload": "Puja"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"name": "Nom",
|
||||
"value": "Valor",
|
||||
"promoted": "Destacat",
|
||||
"promoted_alias": "Àlies",
|
||||
"multiplicity": "Multiplicitat",
|
||||
"label_type": "Tipus",
|
||||
"text": "Text",
|
||||
"number": "Número",
|
||||
"boolean": "Booleà",
|
||||
"date": "Data",
|
||||
"time": "Hora",
|
||||
"url": "URL",
|
||||
"precision": "Precisió",
|
||||
"digits": "dígits",
|
||||
"inheritable": "Heretable",
|
||||
"delete": "Suprimeix",
|
||||
"color_type": "Color"
|
||||
},
|
||||
"rename_label": {
|
||||
"to": "Per"
|
||||
},
|
||||
"move_note": {
|
||||
"to": "a"
|
||||
},
|
||||
"add_relation": {
|
||||
"to": "a"
|
||||
},
|
||||
"rename_relation": {
|
||||
"to": "Per"
|
||||
},
|
||||
"update_relation_target": {
|
||||
"to": "a"
|
||||
},
|
||||
"attachments_actions": {
|
||||
"download": "Descarrega"
|
||||
},
|
||||
"calendar": {
|
||||
"mon": "Dl",
|
||||
"tue": "Dt",
|
||||
"wed": "dc",
|
||||
"thu": "Dj",
|
||||
"fri": "Dv",
|
||||
"sat": "Ds",
|
||||
"sun": "Dg",
|
||||
"january": "Gener",
|
||||
"february": "Febrer",
|
||||
"march": "Març",
|
||||
"april": "Abril",
|
||||
"may": "Maig",
|
||||
"june": "Juny",
|
||||
"july": "Juliol",
|
||||
"august": "Agost",
|
||||
"september": "Setembre",
|
||||
"october": "Octubre",
|
||||
"november": "Novembre",
|
||||
"december": "Desembre"
|
||||
},
|
||||
"global_menu": {
|
||||
"menu": "Menú",
|
||||
"options": "Opcions",
|
||||
"zoom": "Zoom",
|
||||
"advanced": "Avançat",
|
||||
"logout": "Tanca la sessió"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"relation": "relació"
|
||||
},
|
||||
"note_icon": {
|
||||
"category": "Categoria:",
|
||||
"search": "Cerca:"
|
||||
},
|
||||
"basic_properties": {
|
||||
"editable": "Editable",
|
||||
"language": "Llengua"
|
||||
},
|
||||
"book_properties": {
|
||||
"grid": "Graella",
|
||||
"list": "Llista",
|
||||
"collapse": "Replega",
|
||||
"expand": "Desplega",
|
||||
"calendar": "Calendari",
|
||||
"table": "Taula",
|
||||
"board": "Tauler"
|
||||
},
|
||||
"edited_notes": {
|
||||
"deleted": "(suprimit)"
|
||||
},
|
||||
"file_properties": {
|
||||
"download": "Descarrega",
|
||||
"open": "Obre",
|
||||
"title": "Fitxer"
|
||||
},
|
||||
"image_properties": {
|
||||
"download": "Descarrega",
|
||||
"open": "Obre",
|
||||
"title": "Imatge"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"created": "Creat",
|
||||
"modified": "Modificat",
|
||||
"type": "Tipus",
|
||||
"calculate": "calcula"
|
||||
},
|
||||
"note_paths": {
|
||||
"archived": "Arxivat"
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +162,8 @@
|
||||
"inPageSearch": "页面内搜索",
|
||||
"newTabWithActivationNoteLink": "在新标签页打开笔记链接并激活该标签页",
|
||||
"title": "资料表",
|
||||
"newTabNoteLink": "在新标签页开启链接"
|
||||
"newTabNoteLink": "在新标签页开启链接",
|
||||
"editShortcuts": "编辑键盘快捷键"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "导入到笔记",
|
||||
@ -735,9 +736,8 @@
|
||||
"zoom_out_title": "缩小"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} 个反链",
|
||||
"backlinks": "{{count}} 个反链",
|
||||
"relation": "关系"
|
||||
"relation": "关系",
|
||||
"backlink_other": "{{count}} 个反链"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "插入子笔记",
|
||||
@ -764,7 +764,6 @@
|
||||
"grid": "网格",
|
||||
"list": "列表",
|
||||
"collapse_all_notes": "折叠所有笔记",
|
||||
"expand_all_children": "展开所有子项",
|
||||
"collapse": "折叠",
|
||||
"expand": "展开",
|
||||
"invalid_view_type": "无效的查看类型 '{{type}}'",
|
||||
@ -774,7 +773,11 @@
|
||||
"geo-map": "地理地图",
|
||||
"board": "看板",
|
||||
"include_archived_notes": "展示归档笔记",
|
||||
"presentation": "演示"
|
||||
"presentation": "演示",
|
||||
"expand_tooltip": "展开此集合的直接子代(单层深度)。点击右方箭头以查看更多选项。",
|
||||
"expand_first_level": "展开直接子代",
|
||||
"expand_nth_level": "展开 {{depth}} 层",
|
||||
"expand_all_levels": "展开所有层级"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "今天还没有编辑过的笔记...",
|
||||
@ -983,7 +986,9 @@
|
||||
"placeholder": "在这里输入您的代码笔记内容..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "在这里输入您的笔记内容..."
|
||||
"placeholder": "在这里输入您的笔记内容...",
|
||||
"auto-detect-language": "自动检测",
|
||||
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在,请考虑提交错误报告。"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "通过在下面的输入框中输入笔记标题或在树中选择笔记来打开笔记。",
|
||||
@ -1151,7 +1156,10 @@
|
||||
"unit": "字符"
|
||||
},
|
||||
"code_mime_types": {
|
||||
"title": "下拉菜单可用的MIME文件类型"
|
||||
"title": "下拉菜单可用的MIME文件类型",
|
||||
"tooltip_syntax_highlighting": "语法高亮",
|
||||
"tooltip_code_block_syntax": "文本笔记中的代码块",
|
||||
"tooltip_code_note_syntax": "代码笔记"
|
||||
},
|
||||
"vim_key_bindings": {
|
||||
"use_vim_keybindings_in_code_notes": "Vim 快捷键",
|
||||
@ -1463,7 +1471,7 @@
|
||||
"import-into-note": "导入到笔记",
|
||||
"apply-bulk-actions": "应用批量操作",
|
||||
"converted-to-attachments": "{{count}} 个笔记已被转换为附件。",
|
||||
"convert-to-attachment-confirm": "确定要将选中的笔记转换为其父笔记的附件吗?",
|
||||
"convert-to-attachment-confirm": "确定要将选中的笔记转换为其父笔记的附件吗?此操作仅适用于图像笔记,其他笔记将被跳过。",
|
||||
"duplicate": "复制",
|
||||
"open-in-popup": "快速编辑",
|
||||
"archive": "归档",
|
||||
@ -1551,7 +1559,8 @@
|
||||
"refresh-saved-search-results": "刷新保存的搜索结果",
|
||||
"create-child-note": "创建子笔记",
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切换侧边栏"
|
||||
"toggle-sidebar": "切换侧边栏",
|
||||
"dropping-not-allowed": "不允许移动笔记到此处。"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此窗口置顶"
|
||||
@ -1653,9 +1662,6 @@
|
||||
"move-to-available-launchers": "移动到可用启动器",
|
||||
"duplicate-launcher": "复制启动器 <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自动检测"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "代码块",
|
||||
"description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
|
||||
@ -1695,7 +1701,8 @@
|
||||
"copy-link": "复制链接",
|
||||
"paste": "粘贴",
|
||||
"paste-as-plain-text": "以纯文本粘贴",
|
||||
"search_online": "用 {{searchEngine}} 搜索 \"{{term}}\""
|
||||
"search_online": "用 {{searchEngine}} 搜索 \"{{term}}\"",
|
||||
"search_in_trilium": "在 Trilium 中搜索「{{term}}」"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "复制引用到剪贴板",
|
||||
@ -1705,7 +1712,8 @@
|
||||
"open_note_in_new_tab": "在新标签页中打开笔记",
|
||||
"open_note_in_new_split": "在新分屏中打开笔记",
|
||||
"open_note_in_new_window": "在新窗口中打开笔记",
|
||||
"open_note_in_popup": "快速编辑"
|
||||
"open_note_in_popup": "快速编辑",
|
||||
"open_note_in_other_split": "在另一个分屏中打开笔记"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "桌面应用程序",
|
||||
@ -1888,9 +1896,7 @@
|
||||
"indexing_stopped": "索引已停止",
|
||||
"indexing_in_progress": "索引进行中...",
|
||||
"last_indexed": "最后索引时间",
|
||||
"n_notes_queued_0": "{{ count }} 条笔记已加入索引队列",
|
||||
"note_chat": "笔记聊天",
|
||||
"notes_indexed_0": "{{ count }} 条笔记已索引",
|
||||
"sources": "来源",
|
||||
"start_indexing": "开始索引",
|
||||
"use_advanced_context": "使用高级上下文",
|
||||
@ -2089,7 +2095,19 @@
|
||||
"read-only-info": {
|
||||
"read-only-note": "当前正在查看一个只读笔记。",
|
||||
"auto-read-only-note": "这条笔记以只读模式显示便于快速加载。",
|
||||
"auto-read-only-learn-more": "了解更多",
|
||||
"edit-note": "编辑笔记"
|
||||
},
|
||||
"note-color": {
|
||||
"clear-color": "清除笔记颜色",
|
||||
"set-color": "设置笔记颜色",
|
||||
"set-custom-color": "设置自定义笔记颜色"
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "切换至完整编辑器"
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "与服务器通讯错误",
|
||||
"unknown_http_error_content": "状态码: {{statusCode}}\n地址: {{method}} {{url}}\n信息: {{message}}",
|
||||
"traefik_blocks_requests": "如果您使用 Traefik 反向代理,它引入了一项影响与服务器的通信重大更改。"
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,14 +24,6 @@
|
||||
"message": "Uživatelský skript z poznámky s ID \"{{id}}\" a názvem \"{{title}}\" nemohl být spuštěn z důvodu: \n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued_0": "{{ count }} poznámka ve frontě k indexaci",
|
||||
"n_notes_queued_1": "{{ count }} poznámky ve frontě k indexaci",
|
||||
"n_notes_queued_2": "{{ count }} poznámek ve frontě k indexaci",
|
||||
"notes_indexed_0": "{{ count }} poznámka indexována",
|
||||
"notes_indexed_1": "{{ count }} poznámky indexovány",
|
||||
"notes_indexed_2": "{{ count }} poznámek indexováno"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Přidat odkaz",
|
||||
"help_on_links": "Nápověda k odkazům",
|
||||
@ -43,7 +35,7 @@
|
||||
"link_title_arbitrary": "titulek odkazu může být změněn libovolně"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"prefix": "Prefix: ",
|
||||
"prefix": "Předpona: ",
|
||||
"save": "Uložit",
|
||||
"edit_branch_prefix": "Upravit prefix větve",
|
||||
"edit_branch_prefix_multiple": "Upravit prefix větve pro {{count}} větví",
|
||||
@ -63,24 +55,59 @@
|
||||
"bulk_actions_executed": "Hromadné akce byly úspěšně provedeny.",
|
||||
"labels": "Štítky",
|
||||
"relations": "Relace",
|
||||
"other": "Ostatní"
|
||||
"other": "Ostatní",
|
||||
"none_yet": "Zatím žádné akce... přidejte akci kliknutím na jednu z dostupných výše."
|
||||
},
|
||||
"confirm": {
|
||||
"cancel": "Zrušit",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"confirmation": "Potvrzení",
|
||||
"are_you_sure_remove_note": "Opravdu chcete odstranit poznámku „{{title}}“ z mapy vztahů? ",
|
||||
"if_you_dont_check": "Pokud tuto možnost nezaškrtnete, poznámka bude odstraněna pouze z mapy vztahů.",
|
||||
"also_delete_note": "Odstraňte také poznámku"
|
||||
},
|
||||
"delete_notes": {
|
||||
"cancel": "Zrušit",
|
||||
"ok": "OK",
|
||||
"close": "Zavřít"
|
||||
"close": "Zavřít",
|
||||
"delete_notes_preview": "Odstranit náhled poznámek",
|
||||
"delete_all_clones_description": "Odstraňte také všechny klony (lze vrátit zpět v nedávných změnách)",
|
||||
"erase_notes_description": "Normální (měkké) smazání pouze označí poznámky jako smazané a lze je během určité doby obnovit (v dialogovém okně posledních změn). Zaškrtnutím této možnosti se poznámky okamžitě vymažou a nebude možné je obnovit.",
|
||||
"erase_notes_warning": "Trvale smažte poznámky (nelze vrátit zpět), včetně všech klonů. Tím se vynutí opětovné načtení aplikace.",
|
||||
"notes_to_be_deleted": "Následující poznámky budou smazány ({{notesCount}})",
|
||||
"no_note_to_delete": "Žádná poznámka nebude smazána (pouze klony).",
|
||||
"broken_relations_to_be_deleted": "Následující vazby budou přerušeny a smazány ({{relationCount}})",
|
||||
"deleted_relation_text": "Poznámka {{- note}} (bude smazána) je odkazována vazbou {{- relation}} pocházející z {{- source}}."
|
||||
},
|
||||
"export": {
|
||||
"close": "Zavřít"
|
||||
"close": "Zavřít",
|
||||
"export_note_title": "Exportovat poznámku",
|
||||
"export_type_subtree": "Tato poznámka a všechny její odvozené poznámky",
|
||||
"format_html": "HTML – doporučeno, protože zachovává veškeré formátování",
|
||||
"format_html_zip": "HTML v archivu ZIP – toto se doporučuje, protože se tak zachová veškeré formátování.",
|
||||
"format_markdown": "Markdown – zachovává většinu formátování.",
|
||||
"format_opml": "OPML – formát pro výměnu osnov pouze pro text. Formátování, obrázky a soubory nejsou zahrnuty.",
|
||||
"opml_version_1": "OPML v1.0 – pouze prostý text",
|
||||
"opml_version_2": "OPML v2.0 – umožňuje také HTML",
|
||||
"export_type_single": "Pouze tato poznámka bez jejích potomků",
|
||||
"export": "Exportovat",
|
||||
"choose_export_type": "Nejprve vyberte typ exportu",
|
||||
"export_status": "Stav exportu",
|
||||
"export_in_progress": "Export probíhá: {{progressCount}}",
|
||||
"export_finished_successfully": "Export byl úspěšně dokončen.",
|
||||
"format_pdf": "PDF – pro tisk nebo sdílení.",
|
||||
"share-format": "HTML pro publikování na webu – používá stejný motiv jako sdílené poznámky, ale lze jej publikovat jako statický web."
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Klonovat poznámky do...",
|
||||
"help_on_links": "Nápověda k odkazům",
|
||||
"notes_to_clone": "Poznámky na klonování",
|
||||
"search_for_note_by_its_name": "hledat poznámku dle jejího názvu"
|
||||
"search_for_note_by_its_name": "hledat poznámku dle jejího názvu",
|
||||
"prefix_optional": "Předpona (volitelná)",
|
||||
"target_parent_note": "Zaměřit rodičovskou poznámku",
|
||||
"cloned_note_prefix_title": "Klonovaná poznámka se zobrazí ve stromu poznámek s danou předponou",
|
||||
"clone_to_selected_note": "Klonovat vybranou poznámku",
|
||||
"no_path_to_clone_to": "Žádná cest pro klonování.",
|
||||
"note_cloned": "Poznámka: „{{clonedTitle}}“ bylo naklonováno do „{{targetTitle}}“"
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Benutzerdefiniertes Skript konnte nicht geladen werden",
|
||||
"message": "Skript von der Notiz mit der ID \"{{id}}\", und dem Titel \"{{title}}\" konnte nicht ausgeführt werden wegen:\n\n{{message}}"
|
||||
"message": "Skript aus der Notiz \"{{title}}\" mit der ID \"{{id}}\", konnte nicht ausgeführt werden wegen:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@ -39,7 +39,10 @@
|
||||
"help_on_tree_prefix": "Hilfe zum Baumpräfix",
|
||||
"prefix": "Präfix: ",
|
||||
"save": "Speichern",
|
||||
"branch_prefix_saved": "Zweigpräfix wurde gespeichert."
|
||||
"branch_prefix_saved": "Zweigpräfix wurde gespeichert.",
|
||||
"branch_prefix_saved_multiple": "Der Zweigpräfix wurde für {{count}} Zweige gespeichert.",
|
||||
"edit_branch_prefix_multiple": "Branch-Präfix für {{count}} Zweige bearbeiten",
|
||||
"affected_branches": "Betroffene Zweige ({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Massenaktionen",
|
||||
@ -684,7 +687,8 @@
|
||||
"convert_into_attachment_failed": "Konvertierung der Notiz '{{title}}' fehlgeschlagen.",
|
||||
"convert_into_attachment_successful": "Notiz '{{title}}' wurde als Anhang konvertiert.",
|
||||
"convert_into_attachment_prompt": "Bist du dir sicher, dass du die Notiz '{{title}}' in ein Anhang der übergeordneten Notiz konvertieren möchtest?",
|
||||
"print_pdf": "Export als PDF..."
|
||||
"print_pdf": "Export als PDF...",
|
||||
"open_note_on_server": "Öffne Notiz auf dem Server"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
|
||||
@ -728,9 +732,9 @@
|
||||
"zoom_out_title": "Herauszoomen"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Rückverlinkung",
|
||||
"backlinks": "{{count}} Rückverlinkungen",
|
||||
"relation": "Beziehung"
|
||||
"relation": "Beziehung",
|
||||
"backlink_one": "{{count}} Rückverlinkung",
|
||||
"backlink_other": "{{count}} Rückverlinkungen"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Untergeordnete Notiz einfügen",
|
||||
@ -757,7 +761,6 @@
|
||||
"grid": "Gitter",
|
||||
"list": "Liste",
|
||||
"collapse_all_notes": "Alle Notizen einklappen",
|
||||
"expand_all_children": "Unternotizen ausklappen",
|
||||
"collapse": "Einklappen",
|
||||
"expand": "Ausklappen",
|
||||
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
|
||||
@ -767,7 +770,11 @@
|
||||
"geo-map": "Weltkarte",
|
||||
"board": "Tafel",
|
||||
"include_archived_notes": "Zeige archivierte Notizen",
|
||||
"presentation": "Präsentation"
|
||||
"presentation": "Präsentation",
|
||||
"expand_all_levels": "Alle Ebenen erweitern",
|
||||
"expand_tooltip": "Erweitert die direkten Unterelemente dieser Sammlung (eine Ebene tiefer). Für weitere Optionen auf den Pfeil rechts klicken.",
|
||||
"expand_first_level": "Direkte Unterelemente erweitern",
|
||||
"expand_nth_level": "{{depth}} Ebenen erweitern"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
|
||||
@ -976,7 +983,9 @@
|
||||
"placeholder": "Gebe hier den Inhalt deiner Codenotiz ein..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Gebe hier den Inhalt deiner Notiz ein..."
|
||||
"placeholder": "Gebe hier den Inhalt deiner Notiz ein...",
|
||||
"auto-detect-language": "Automatisch erkannt",
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Öffne eine Notiz, indem du den Titel der Notiz in die Eingabe unten eingibst oder eine Notiz in der Baumstruktur auswählst.",
|
||||
@ -1104,7 +1113,8 @@
|
||||
"title": "Inhaltsbreite",
|
||||
"default_description": "Trilium begrenzt standardmäßig die maximale Inhaltsbreite, um die Lesbarkeit für maximierte Bildschirme auf Breitbildschirmen zu verbessern.",
|
||||
"max_width_label": "Maximale Inhaltsbreite in Pixel",
|
||||
"max_width_unit": "Pixel"
|
||||
"max_width_unit": "Pixel",
|
||||
"centerContent": "Inhalt zentriert halten"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Native Titelleiste (App-Neustart erforderlich)",
|
||||
@ -1143,7 +1153,10 @@
|
||||
"unit": "Zeichen"
|
||||
},
|
||||
"code_mime_types": {
|
||||
"title": "Verfügbare MIME-Typen im Dropdown-Menü"
|
||||
"title": "Verfügbare MIME-Typen im Dropdown-Menü",
|
||||
"tooltip_syntax_highlighting": "Syntaxhervorhebung",
|
||||
"tooltip_code_block_syntax": "Code-Blöcke in Textnotizen",
|
||||
"tooltip_code_note_syntax": "Code-Notizen"
|
||||
},
|
||||
"vim_key_bindings": {
|
||||
"use_vim_keybindings_in_code_notes": "Verwende VIM-Tastenkombinationen in Codenotizen (kein Ex-Modus)",
|
||||
@ -1510,7 +1523,8 @@
|
||||
"refresh-saved-search-results": "Gespeicherte Suchergebnisse aktualisieren",
|
||||
"create-child-note": "Unternotiz anlegen",
|
||||
"unhoist": "Fokus verlassen",
|
||||
"toggle-sidebar": "Seitenleiste ein-/ausblenden"
|
||||
"toggle-sidebar": "Seitenleiste ein-/ausblenden",
|
||||
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Dieses Fenster immer oben halten"
|
||||
@ -1612,9 +1626,6 @@
|
||||
"move-to-available-launchers": "Zu verfügbaren Launchern verschieben",
|
||||
"duplicate-launcher": "Launcher duplizieren <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Automatisch erkannt"
|
||||
},
|
||||
"highlighting": {
|
||||
"description": "Steuert die Syntaxhervorhebung für Codeblöcke in Textnotizen, Code-Notizen sind nicht betroffen.",
|
||||
"color-scheme": "Farbschema",
|
||||
@ -1654,7 +1665,8 @@
|
||||
"copy-link": "Link kopieren",
|
||||
"paste": "Einfügen",
|
||||
"paste-as-plain-text": "Als unformatierten Text einfügen",
|
||||
"search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten"
|
||||
"search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten",
|
||||
"search_in_trilium": "Suche nach \"{{term}}\" in Trilium"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "Referenz in Zwischenablage kopieren",
|
||||
@ -1715,10 +1727,6 @@
|
||||
"help_title": "Zeige mehr Informationen zu diesem Fenster"
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued": "{{ count }} Notiz zur Indizierung vorgemerkt",
|
||||
"n_notes_queued_plural": "{{ count }} Notizen zur Indizierung vorgemerkt",
|
||||
"notes_indexed": "{{ count }} Notiz indiziert",
|
||||
"notes_indexed_plural": "{{ count }} Notizen indiziert",
|
||||
"not_started": "Nicht gestartet",
|
||||
"title": "KI Einstellungen",
|
||||
"processed_notes": "Verarbeitete Notizen",
|
||||
@ -2028,7 +2036,8 @@
|
||||
"new-item-placeholder": "Notiz Titel eingeben...",
|
||||
"add-column-placeholder": "Spaltenname eingeben...",
|
||||
"edit-note-title": "Klicke zum Editieren des Notiz-Titels",
|
||||
"edit-column-title": "Klicke zum Editieren des Spalten-Titels"
|
||||
"edit-column-title": "Klicke zum Editieren des Spalten-Titels",
|
||||
"column-already-exists": "Die Spalte ist auf dem Board bereits vorhanden."
|
||||
},
|
||||
"command_palette": {
|
||||
"tree-action-name": "Struktur: {{name}}",
|
||||
@ -2078,5 +2087,18 @@
|
||||
"edit-slide": "Folie bearbeiten",
|
||||
"start-presentation": "Präsentation starten",
|
||||
"slide-overview": "Übersicht der Folien ein-/ausblenden"
|
||||
},
|
||||
"read-only-info": {
|
||||
"read-only-note": "Aktuelle Notiz wird im Lese-Modus angezeigt.",
|
||||
"auto-read-only-note": "Diese Notiz wird im Nur-Lesen-Modus angezeigt, um ein schnelleres Laden zu ermöglichen.",
|
||||
"edit-note": "Notiz bearbeiten"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "Notiz löschen..."
|
||||
},
|
||||
"note-color": {
|
||||
"clear-color": "Notizfarbe entfernen",
|
||||
"set-color": "Notizfarbe wählen",
|
||||
"set-custom-color": "Eigene Notizfarbe wählen"
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,11 +14,5 @@
|
||||
"title": "Κρίσιμο σφάλμα",
|
||||
"message": "Συνέβη κάποιο κρίσιμο σφάλμα, το οποίο δεν επιτρέπει στην εφαρμογή χρήστη να ξεκινήσει:\n\n{{message}}\n\nΤο πιθανότερο είναι να προκλήθηκε από κάποιο script που απέτυχε απρόοπτα. Δοκιμάστε να ξεκινήσετε την εφαρμογή σε ασφαλή λειτουργία για να λύσετε το πρόβλημα."
|
||||
}
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued": "{{ count }} σημείωση στην ουρά για εύρεση",
|
||||
"n_notes_queued_plural": "{{ count }} σημειώσεις στην ουρά για εύρεση",
|
||||
"notes_indexed": "{{ count }} σημείωση με ευρετήριο",
|
||||
"notes_indexed_plural": "{{ count }} σημειώσεις με ευρετήριο"
|
||||
}
|
||||
}
|
||||
|
||||
73
apps/client/src/translations/en-GB/translation.json
Normal file
73
apps/client/src/translations/en-GB/translation.json
Normal file
@ -0,0 +1,73 @@
|
||||
{
|
||||
"import": {
|
||||
"safeImportTooltip": "Trilium <code>.zip</code> export files can contain executable scripts which may contain harmful behaviour. Safe import will deactivate automatic execution of all imported scripts. Uncheck \"Safe import\" only if the imported archive is supposed to contain executable scripts and you completely trust the contents of the import file.",
|
||||
"shrinkImagesTooltip": "<p>If you check this option, Trilium will attempt to shrink the imported images by scaling and optimisation which may affect the perceived image quality. If unchecked, images will be imported without changes.</p><p>This doesn't apply to <code>.zip</code> imports with metadata since it is assumed these files are already optimised.</p>",
|
||||
"codeImportedAsCode": "Import recognised code files (e.g. <code>.json</code>) as code notes if it's unclear from metadata"
|
||||
},
|
||||
"upload_attachments": {
|
||||
"tooltip": "If you check this option, Trilium will attempt to shrink the uploaded images by scaling and optimisation which may affect the perceived image quality. If unchecked, images will be uploaded without changes."
|
||||
},
|
||||
"attribute_detail": {
|
||||
"auto_read_only_disabled": "text/code notes can be set automatically into read mode when they are too large. You can disable this behaviour on per-note basis by adding this label to the note",
|
||||
"workspace_tab_background_color": "CSS colour used in the note tab when hoisted to this note",
|
||||
"color": "defines colour of the note in note tree, links etc. Use any valid CSS colour value like 'red' or #a13d5f",
|
||||
"color_type": "Colour"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"error_unrecognized_command": "Unrecognised command {{command}}"
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"remove_color": "Remove the colour label"
|
||||
},
|
||||
"max_content_width": {
|
||||
"centerContent": "Keep content centred"
|
||||
},
|
||||
"theme": {
|
||||
"auto_theme": "Legacy (Follow system colour scheme)",
|
||||
"triliumnext": "Trilium (Follow system colour scheme)"
|
||||
},
|
||||
"search_engine": {
|
||||
"custom_name_placeholder": "Customise search engine name",
|
||||
"custom_url_placeholder": "Customise search engine url"
|
||||
},
|
||||
"highlights_list": {
|
||||
"description": "You can customise the highlights list displayed in the right panel:",
|
||||
"color": "Coloured text",
|
||||
"bg_color": "Text with background colour"
|
||||
},
|
||||
"table_of_contents": {
|
||||
"description": "Table of contents will appear in text notes when the note has more than a defined number of headings. You can customise this number:"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"description": "Customise the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens."
|
||||
},
|
||||
"i18n": {
|
||||
"title": "Localisation"
|
||||
},
|
||||
"attachment_detail_2": {
|
||||
"unrecognized_role": "Unrecognised attachment role '{{role}}'."
|
||||
},
|
||||
"ai_llm": {
|
||||
"reprocess_index_started": "Search index optimisation started in the background",
|
||||
"index_rebuilding": "Optimising index ({{percentage}}%)",
|
||||
"index_rebuild_complete": "Index optimisation complete"
|
||||
},
|
||||
"highlighting": {
|
||||
"color-scheme": "Colour Scheme"
|
||||
},
|
||||
"code_theme": {
|
||||
"color-scheme": "Colour scheme"
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of colour to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer."
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Colour scheme for code blocks in text notes",
|
||||
"related_code_notes": "Colour scheme for code notes"
|
||||
},
|
||||
"note-color": {
|
||||
"clear-color": "Clear note colour",
|
||||
"set-color": "Set note colour",
|
||||
"set-custom-color": "Set custom note colour"
|
||||
}
|
||||
}
|
||||
@ -112,6 +112,7 @@
|
||||
},
|
||||
"help": {
|
||||
"title": "Cheatsheet",
|
||||
"editShortcuts": "Edit keyboard shortcuts",
|
||||
"noteNavigation": "Note navigation",
|
||||
"goUpDown": "go up/down in the list of notes",
|
||||
"collapseExpand": "collapse/expand node",
|
||||
@ -204,7 +205,8 @@
|
||||
"info": {
|
||||
"modalTitle": "Info message",
|
||||
"closeButton": "Close",
|
||||
"okButton": "OK"
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "Copy to clipboard"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_placeholder": "Search for note by its name or type > for commands...",
|
||||
@ -735,8 +737,8 @@
|
||||
"zoom_out_title": "Zoom Out"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Backlink",
|
||||
"backlinks": "{{count}} Backlinks",
|
||||
"backlink_one": "{{count}} Backlink",
|
||||
"backlink_other": "{{count}} Backlinks",
|
||||
"relation": "relation"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
@ -764,9 +766,12 @@
|
||||
"grid": "Grid",
|
||||
"list": "List",
|
||||
"collapse_all_notes": "Collapse all notes",
|
||||
"expand_all_children": "Expand all children",
|
||||
"expand_tooltip": "Expands the direct children of this collection (one level deep). For more options, press the arrow on the right.",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"expand_first_level": "Expand direct children",
|
||||
"expand_nth_level": "Expand {{depth}} levels",
|
||||
"expand_all_levels": "Expand all levels",
|
||||
"book_properties": "Collection Properties",
|
||||
"invalid_view_type": "Invalid view type '{{type}}'",
|
||||
"calendar": "Calendar",
|
||||
@ -983,7 +988,14 @@
|
||||
"placeholder": "Type the content of your code note here..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Type the content of your note here..."
|
||||
"placeholder": "Type the content of your note here...",
|
||||
"editor_crashed_title": "The text editor crashed",
|
||||
"editor_crashed_content": "Your content was recovered successfully, but a few of your most recent changes may not have been saved.",
|
||||
"editor_crashed_details_button": "View more details...",
|
||||
"editor_crashed_details_intro": "If you experience this error several times, consider reporting it on GitHub by pasting the information below.",
|
||||
"editor_crashed_details_title": "Technical information",
|
||||
"auto-detect-language": "Auto-detected",
|
||||
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Open a note by typing the note's title into the input below or choose a note in the tree.",
|
||||
@ -1267,11 +1279,7 @@
|
||||
"indexing_stopped": "Indexing stopped",
|
||||
"indexing_in_progress": "Indexing in progress...",
|
||||
"last_indexed": "Last Indexed",
|
||||
"n_notes_queued": "{{ count }} note queued for indexing",
|
||||
"n_notes_queued_plural": "{{ count }} notes queued for indexing",
|
||||
"note_chat": "Note Chat",
|
||||
"notes_indexed": "{{ count }} note indexed",
|
||||
"notes_indexed_plural": "{{ count }} notes indexed",
|
||||
"sources": "Sources",
|
||||
"start_indexing": "Start Indexing",
|
||||
"use_advanced_context": "Use Advanced Context",
|
||||
@ -1309,7 +1317,10 @@
|
||||
"title": "Editor"
|
||||
},
|
||||
"code_mime_types": {
|
||||
"title": "Available MIME types in the dropdown"
|
||||
"title": "Available MIME types in the dropdown",
|
||||
"tooltip_syntax_highlighting": "Syntax highlighting",
|
||||
"tooltip_code_block_syntax": "Code blocks in Text notes",
|
||||
"tooltip_code_note_syntax": "Code notes"
|
||||
},
|
||||
"vim_key_bindings": {
|
||||
"use_vim_keybindings_in_code_notes": "Vim keybindings",
|
||||
@ -1630,7 +1641,7 @@
|
||||
"import-into-note": "Import into note",
|
||||
"apply-bulk-actions": "Apply bulk actions",
|
||||
"converted-to-attachments": "{{count}} notes have been converted to attachments.",
|
||||
"convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?",
|
||||
"convert-to-attachment-confirm": "Are you sure you want to convert the selected notes into attachments of their parent notes? This operation only applies to Image notes, other notes will be skipped.",
|
||||
"open-in-popup": "Quick edit"
|
||||
},
|
||||
"shared_info": {
|
||||
@ -1641,7 +1652,6 @@
|
||||
"read-only-info": {
|
||||
"read-only-note": "Currently viewing a read-only note.",
|
||||
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
|
||||
"auto-read-only-learn-more": "Learn more",
|
||||
"edit-note": "Edit note"
|
||||
},
|
||||
"note_types": {
|
||||
@ -1721,7 +1731,8 @@
|
||||
"refresh-saved-search-results": "Refresh saved search results",
|
||||
"create-child-note": "Create child note",
|
||||
"unhoist": "Unhoist",
|
||||
"toggle-sidebar": "Toggle sidebar"
|
||||
"toggle-sidebar": "Toggle sidebar",
|
||||
"dropping-not-allowed": "Dropping notes into this location is not allowed."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep Window on Top"
|
||||
@ -1823,9 +1834,6 @@
|
||||
"move-to-available-launchers": "Move to available launchers",
|
||||
"duplicate-launcher": "Duplicate launcher <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Auto-detected"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Code Blocks",
|
||||
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
|
||||
@ -1865,6 +1873,7 @@
|
||||
"copy-link": "Copy link",
|
||||
"paste": "Paste",
|
||||
"paste-as-plain-text": "Paste as plain text",
|
||||
"search_in_trilium": "Search for \"{{term}}\" in Trilium",
|
||||
"search_online": "Search for \"{{term}}\" with {{searchEngine}}"
|
||||
},
|
||||
"image_context_menu": {
|
||||
@ -1874,6 +1883,7 @@
|
||||
"link_context_menu": {
|
||||
"open_note_in_new_tab": "Open note in a new tab",
|
||||
"open_note_in_new_split": "Open note in a new split",
|
||||
"open_note_in_other_split": "Open note in the other split",
|
||||
"open_note_in_new_window": "Open note in a new window",
|
||||
"open_note_in_popup": "Quick edit"
|
||||
},
|
||||
@ -1962,7 +1972,8 @@
|
||||
"button_title": "Export diagram as PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "The diagram could not be exported to PNG."
|
||||
"export_to_png": "The diagram could not be exported to PNG.",
|
||||
"export_to_svg": "The diagram could not be exported to SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Appearance",
|
||||
@ -2093,5 +2104,18 @@
|
||||
},
|
||||
"collections": {
|
||||
"rendering_error": "Unable to show content due to an error."
|
||||
},
|
||||
"note-color": {
|
||||
"clear-color": "Clear note color",
|
||||
"set-color": "Set note color",
|
||||
"set-custom-color": "Set custom note color"
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Switch to full editor"
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "Communication error with the server",
|
||||
"unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
|
||||
"traefik_blocks_requests": "If you are using the Traefik reverse proxy, it introduced a breaking change which affects the communication with the server."
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user