mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 23:14:24 +01:00
Compare commits
248 Commits
0c3b552678
...
8f73d3228d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f73d3228d | ||
|
|
67d2175ce9 | ||
|
|
8a3283f1ea | ||
|
|
2fb47fc186 | ||
|
|
4530c9a40c | ||
|
|
a867a25d5f | ||
|
|
10910ac2ed | ||
|
|
514f5a0c81 | ||
|
|
6b18ed6477 | ||
|
|
e07b6cc409 | ||
|
|
5ae67fda7f | ||
|
|
1a03c0ca9f | ||
|
|
dc211b4d00 | ||
|
|
2e767ffde1 | ||
|
|
9f74a54c0d | ||
|
|
a7870495ac | ||
|
|
a8ec323ea8 | ||
|
|
4b7d243406 | ||
|
|
1855588270 | ||
|
|
e545e0c3a5 | ||
|
|
b24a0f1595 | ||
|
|
3f2bfc3050 | ||
|
|
66f100d534 | ||
|
|
e641d5ba0f | ||
|
|
6f8cebf954 | ||
|
|
2d2f2457cb | ||
|
|
68fa243af4 | ||
|
|
9cb133c8a6 | ||
|
|
33f6c75917 | ||
|
|
49acd5e8c2 | ||
|
|
85266adf20 | ||
|
|
27edea8380 | ||
|
|
dccddb8d43 | ||
|
|
5f9d7a223a | ||
|
|
29b71262d6 | ||
|
|
0cfcbcf5df | ||
|
|
e1258384d8 | ||
|
|
8837eddd40 | ||
|
|
f919d46e9a | ||
|
|
98094fac63 | ||
|
|
f94ccee252 | ||
|
|
7c4b338539 | ||
|
|
d88431f9e3 | ||
|
|
5f86068489 | ||
|
|
36bc226674 | ||
|
|
7947745218 | ||
|
|
19697aabfb | ||
|
|
87af23598d | ||
|
|
4ed7966a5a | ||
|
|
8d86b5c7e6 | ||
|
|
cdf9458962 | ||
|
|
83df37c2d1 | ||
|
|
bc7b3165c8 | ||
|
|
c332e9764a | ||
|
|
004d48d36c | ||
|
|
28b263a445 | ||
|
|
e0e9310907 | ||
|
|
68b9159b2b | ||
|
|
f5940cbf70 | ||
|
|
bfb143bb51 | ||
|
|
64662d5215 | ||
|
|
31cedad976 | ||
|
|
9682df6240 | ||
|
|
a718908385 | ||
|
|
12ac5147d3 | ||
|
|
17291ff61d | ||
|
|
f3e334470e | ||
|
|
9407051f1e | ||
|
|
08a6d36153 | ||
|
|
f906fb9b4c | ||
|
|
b4a6356724 | ||
|
|
8eca14069a | ||
|
|
1af0477ac0 | ||
|
|
43920f12ae | ||
|
|
5a0beec6cb | ||
|
|
98241fb54b | ||
|
|
3051664228 | ||
|
|
1ed774365c | ||
|
|
f2e33dfd58 | ||
|
|
90b5282b39 | ||
|
|
d520fc46b9 | ||
|
|
e69b5988ec | ||
|
|
3cdc1ba794 | ||
|
|
25e1008c5c | ||
|
|
a093862311 | ||
|
|
53057ea9fc | ||
|
|
94db96de3e | ||
|
|
60e4fbbf75 | ||
|
|
d35dd67632 | ||
|
|
8813985c68 | ||
|
|
538c98b587 | ||
|
|
389c7029cf | ||
|
|
d47f9e1131 | ||
|
|
c0a8d29756 | ||
|
|
668fd34af6 | ||
|
|
8aa08cf8fe | ||
|
|
16c04f5ae4 | ||
|
|
32c16021c4 | ||
|
|
7713c1173a | ||
|
|
8018f400c3 | ||
|
|
79c8293881 | ||
|
|
db5652623b | ||
|
|
0f7a48b323 | ||
|
|
309fbab2e6 | ||
|
|
99da145d65 | ||
|
|
415d2826c6 | ||
|
|
7787e7085e | ||
|
|
4ab8417168 | ||
|
|
a77e76d5c6 | ||
|
|
ca6660e2ff | ||
|
|
af94410c55 | ||
|
|
b47bc50147 | ||
|
|
5ff77c16ab | ||
|
|
11618260cf | ||
|
|
63f9006d17 | ||
|
|
7779acc7bc | ||
|
|
aacd92eee3 | ||
|
|
1bf8be2874 | ||
|
|
66f2d0c7dc | ||
|
|
597d952254 | ||
|
|
288595ce5d | ||
|
|
c89e8c78d3 | ||
|
|
81a37e3fc4 | ||
|
|
368c590976 | ||
|
|
6e982e646d | ||
|
|
030582b2d5 | ||
|
|
7dd4b10a96 | ||
|
|
b055e79b4c | ||
|
|
ba980aa93f | ||
|
|
15baf04ce9 | ||
|
|
4dc2587817 | ||
|
|
d5e046c289 | ||
|
|
ef8073ac58 | ||
|
|
38c9d25214 | ||
|
|
e2f0e4089f | ||
|
|
80ce2c04ed | ||
|
|
4ebd82beeb | ||
|
|
8cc43cd9a6 | ||
|
|
15190abb69 | ||
|
|
0b28159e8e | ||
|
|
d9e8f8e69b | ||
|
|
fa224e46bc | ||
|
|
06320953e8 | ||
|
|
2163334c4f | ||
|
|
f5d180af6b | ||
|
|
d676084cb3 | ||
|
|
0cb5941be0 | ||
|
|
732494dfc5 | ||
|
|
b8748b856a | ||
|
|
cc71f15700 | ||
|
|
124ef640b1 | ||
|
|
f5e3df0cd2 | ||
|
|
c8431181c8 | ||
|
|
07fb5ab017 | ||
|
|
6735b257b4 | ||
|
|
cef242a9ce | ||
|
|
2923d917e5 | ||
|
|
1d1639e5e1 | ||
|
|
6ab05fdb76 | ||
|
|
91ae8c0aaf | ||
|
|
fcb69c0190 | ||
|
|
2cf6fe4352 | ||
|
|
e3a2623a53 | ||
|
|
e0c0a423c1 | ||
|
|
fd99246c49 | ||
|
|
d247edd870 | ||
|
|
9a76a9069c | ||
|
|
8e1d796870 | ||
|
|
8b0d4e5c3b | ||
|
|
b9e257a39d | ||
|
|
e7eaa5fd58 | ||
|
|
c9aa992e73 | ||
|
|
f325930f68 | ||
|
|
1346ffb77e | ||
|
|
3378746530 | ||
|
|
ce2d94f04e | ||
|
|
b3c2a1e6c5 | ||
|
|
dbf63787da | ||
|
|
88a7ebef69 | ||
|
|
a716151dd9 | ||
|
|
7462f1b7a5 | ||
|
|
ec76b9dc5c | ||
|
|
79cd96ade9 | ||
|
|
a5b84406be | ||
|
|
8c1a04c4b2 | ||
|
|
ee81037173 | ||
|
|
453349be26 | ||
|
|
81a9e06b23 | ||
|
|
7d8af0f252 | ||
|
|
a68cd7526b | ||
|
|
470ca3b6dc | ||
|
|
e8bae61afc | ||
|
|
c1f663a200 | ||
|
|
22b2e21df0 | ||
|
|
5f19710791 | ||
|
|
d3f3ff4eab | ||
|
|
5af7425cae | ||
|
|
fe10c9f8c8 | ||
|
|
cd2a085d00 | ||
|
|
3c61626370 | ||
|
|
351fe5848f | ||
|
|
ca7bbefbdc | ||
|
|
7094f71e32 | ||
|
|
88b5e9db87 | ||
|
|
b4ab07bd78 | ||
|
|
fd6ad6dce3 | ||
|
|
ab97068a1d | ||
|
|
70fe3b9773 | ||
|
|
1fe8079fd5 | ||
|
|
80627997d1 | ||
|
|
12abdcaf6c | ||
|
|
a6ed4d92c9 | ||
|
|
0471640f54 | ||
|
|
4cf3e82fb5 | ||
|
|
fbbe999806 | ||
|
|
76af488d35 | ||
|
|
a54d2a5f22 | ||
|
|
a1df075194 | ||
|
|
4de2182b40 | ||
|
|
6fa88123f1 | ||
|
|
f81dbde15e | ||
|
|
484fbc6b9d | ||
|
|
8ba30135a1 | ||
|
|
ba5a72fdad | ||
|
|
70b39ddadf | ||
|
|
8200c0b0ab | ||
|
|
59ebfa6cc7 | ||
|
|
57b694162d | ||
|
|
2e6bdc225f | ||
|
|
8ced689432 | ||
|
|
53a8f6b4c0 | ||
|
|
9ae1a55896 | ||
|
|
5ecafe214f | ||
|
|
8baf0ad6af | ||
|
|
3cc64b5764 | ||
|
|
19cf07564f | ||
|
|
5847ce5c14 | ||
|
|
a7ad45635e | ||
|
|
781215394e | ||
|
|
0aafdca999 | ||
|
|
18d3cb6f0c | ||
|
|
263a96e8b7 | ||
|
|
27d5009486 | ||
|
|
4d1a91baa6 | ||
|
|
1898efa282 | ||
|
|
648ab4d736 | ||
|
|
407cac588a | ||
|
|
210dcfb989 |
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)
|
||||
13
.github/workflows/nightly.yml
vendored
13
.github/workflows/nightly.yml
vendored
@ -45,6 +45,19 @@ 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@v6
|
||||
|
||||
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@ -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
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"fs-extra": "11.3.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"typedoc": "0.28.14",
|
||||
"typedoc": "0.28.15",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -487,7 +487,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: {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -29,9 +29,11 @@ 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";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
span.keyboard-shortcut,
|
||||
kbd {
|
||||
display: none;
|
||||
}
|
||||
@ -141,6 +143,7 @@ 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 SplitNoteContainer(() =>
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
@ -171,6 +174,7 @@ export default class MobileLayout {
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.child(
|
||||
new FlexContainer("column")
|
||||
.contentSized()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ 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",
|
||||
@ -128,7 +129,6 @@ function CustomColorCell(props: ColorCellProps) {
|
||||
const colorInput = useRef<HTMLInputElement>(null);
|
||||
const colorInputDebouncer = useRef<Debouncer<string | null> | null>(null);
|
||||
const callbackRef = useRef(props.onSelect);
|
||||
const isSafari = useRef(/^((?!chrome|android).)*safari/i.test(navigator.userAgent));
|
||||
|
||||
useEffect(() => {
|
||||
colorInputDebouncer.current = new Debouncer(250, (color) => {
|
||||
@ -160,11 +160,11 @@ function CustomColorCell(props: ColorCellProps) {
|
||||
}, [pickedColor]);
|
||||
|
||||
return <div style={`--foreground: ${getForegroundColor(props.color)};`}
|
||||
onClick={(e) => {
|
||||
// The color picker dropdown will close on Safari if the parent context menu is
|
||||
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.
|
||||
isSafari.current && e.stopPropagation();
|
||||
}}>
|
||||
e.stopPropagation();
|
||||
} : undefined}>
|
||||
<ColorCell {...props}
|
||||
color={pickedColor}
|
||||
className={clsx("custom-color-cell", {
|
||||
|
||||
@ -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,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) {
|
||||
|
||||
@ -207,7 +207,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";
|
||||
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dropdown-menu:not(.static).calendar-dropdown-menu {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.calendar-dropdown-widget {
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
@ -26,6 +26,10 @@
|
||||
--bs-body-color: var(--main-text-color) !important;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--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;
|
||||
}
|
||||
@ -581,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);
|
||||
@ -706,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;
|
||||
}
|
||||
@ -1006,10 +1015,16 @@ div[data-notify="container"] {
|
||||
font-family: var(--monospace-font-family);
|
||||
}
|
||||
|
||||
svg.ck-icon .note-icon {
|
||||
svg.ck-icon {
|
||||
&.ck-icon_inherit-color path[fill="#333"] {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
&.note-icon {
|
||||
color: var(--main-text-color);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.ck-content {
|
||||
--ck-content-font-family: var(--detail-font-family);
|
||||
@ -1117,10 +1132,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 */
|
||||
}
|
||||
@ -1316,7 +1327,7 @@ 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;
|
||||
@ -1379,6 +1390,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;
|
||||
}
|
||||
@ -1554,12 +1579,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 {
|
||||
@ -1664,46 +1692,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 */
|
||||
@ -2598,3 +2586,32 @@ iframe.print-iframe {
|
||||
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 {
|
||||
@ -92,3 +95,7 @@ html {
|
||||
.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;
|
||||
@ -322,13 +311,37 @@ body.desktop .dropdown-menu.static .dropdown-item.active {
|
||||
}
|
||||
|
||||
/* #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: var(--dropdown-menu-padding-vertical);
|
||||
top: 0.5em;
|
||||
right: var(--dropdown-menu-padding-horizontal);
|
||||
transform: translateX(50%) rotate(90deg);
|
||||
}
|
||||
@ -342,7 +355,7 @@ body.mobile .dropdown-menu {
|
||||
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(--main-border-color) !important;
|
||||
border-bottom: 1px solid var(--menu-item-delimiter-color) !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@ -366,6 +379,49 @@ body.mobile .dropdown-menu {
|
||||
.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 */
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -146,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
|
||||
*/
|
||||
|
||||
@ -643,7 +643,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 */
|
||||
|
||||
@ -345,7 +345,7 @@ body[dir=ltr] #launcher-container {
|
||||
*/
|
||||
|
||||
.calendar-dropdown-widget {
|
||||
padding: 12px;
|
||||
padding: 18px;
|
||||
color: var(--calendar-color);
|
||||
user-select: none;
|
||||
}
|
||||
@ -1448,6 +1448,14 @@ 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
|
||||
*/
|
||||
|
||||
@ -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": "الفئة:",
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Sobre Trilium Notes",
|
||||
"homepage": "Pàgina principal:"
|
||||
"homepage": "Pàgina principal:",
|
||||
"app_version": "Versió de l'aplicació:",
|
||||
"db_version": "Versió de la base de dades:"
|
||||
},
|
||||
"add_link": {
|
||||
"note": "Nota"
|
||||
|
||||
@ -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": "插入子笔记",
|
||||
@ -1557,7 +1557,8 @@
|
||||
"refresh-saved-search-results": "刷新保存的搜索结果",
|
||||
"create-child-note": "创建子笔记",
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切换侧边栏"
|
||||
"toggle-sidebar": "切换侧边栏",
|
||||
"dropping-not-allowed": "不允许移动笔记到此处。"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此窗口置顶"
|
||||
@ -1660,7 +1661,8 @@
|
||||
"duplicate-launcher": "复制启动器 <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自动检测"
|
||||
"auto-detect-language": "自动检测",
|
||||
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在,请考虑提交错误报告。"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "代码块",
|
||||
@ -1895,9 +1897,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": "使用高级上下文",
|
||||
@ -2096,12 +2096,14 @@
|
||||
"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": "切换至完整编辑器"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
@ -41,7 +41,8 @@
|
||||
"save": "Speichern",
|
||||
"branch_prefix_saved": "Zweigpräfix wurde gespeichert.",
|
||||
"branch_prefix_saved_multiple": "Der Zweigpräfix wurde für {{count}} Zweige gespeichert.",
|
||||
"edit_branch_prefix_multiple": "Präfix für {{count}} Zweige bearbeiten"
|
||||
"edit_branch_prefix_multiple": "Branch-Präfix für {{count}} Zweige bearbeiten",
|
||||
"affected_branches": "Betroffene Zweige ({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Massenaktionen",
|
||||
@ -731,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",
|
||||
@ -770,7 +771,10 @@
|
||||
"board": "Tafel",
|
||||
"include_archived_notes": "Zeige archivierte Notizen",
|
||||
"presentation": "Präsentation",
|
||||
"expand_all_levels": "Alle Ebenen erweitern"
|
||||
"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...",
|
||||
@ -1517,7 +1521,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"
|
||||
@ -1620,7 +1625,8 @@
|
||||
"duplicate-launcher": "Launcher duplizieren <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Automatisch erkannt"
|
||||
"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."
|
||||
},
|
||||
"highlighting": {
|
||||
"description": "Steuert die Syntaxhervorhebung für Codeblöcke in Textnotizen, Code-Notizen sind nicht betroffen.",
|
||||
@ -1723,10 +1729,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",
|
||||
@ -2089,9 +2091,8 @@
|
||||
"slide-overview": "Übersicht der Folien ein-/ausblenden"
|
||||
},
|
||||
"read-only-info": {
|
||||
"read-only-note": "Aktuell wird eine Notiz nur im Lese-Modus angezeigt.",
|
||||
"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.",
|
||||
"auto-read-only-learn-more": "Mehr erfahren",
|
||||
"edit-note": "Notiz bearbeiten"
|
||||
},
|
||||
"calendar_view": {
|
||||
|
||||
@ -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",
|
||||
@ -735,8 +736,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": {
|
||||
@ -1270,11 +1271,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",
|
||||
@ -1647,7 +1644,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": {
|
||||
@ -1883,6 +1879,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"
|
||||
},
|
||||
@ -2107,5 +2104,8 @@
|
||||
"clear-color": "Clear note color",
|
||||
"set-color": "Set note color",
|
||||
"set-custom-color": "Set custom note color"
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Switch to full editor"
|
||||
}
|
||||
}
|
||||
|
||||
@ -735,9 +735,10 @@
|
||||
"zoom_out_title": "Alejar"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Vínculo de retroceso",
|
||||
"backlinks": "{{count}} vínculos de retroceso",
|
||||
"relation": "relación"
|
||||
"relation": "relación",
|
||||
"backlink_one": "{{count}} Vínculo de retroceso",
|
||||
"backlink_many": "",
|
||||
"backlink_other": "{{count}} vínculos de retroceso"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Insertar subnota",
|
||||
@ -1262,12 +1263,7 @@
|
||||
"indexing_stopped": "Indexado detenido",
|
||||
"indexing_in_progress": "Indexado en progreso...",
|
||||
"last_indexed": "Último indexado",
|
||||
"n_notes_queued_0": "{{ count }} nota agregada a la cola para indexar",
|
||||
"n_notes_queued_1": "{{ count }} notas agregadas a la cola para indexar",
|
||||
"n_notes_queued_2": "",
|
||||
"note_chat": "Chat de nota",
|
||||
"notes_indexed": "{{ count }} nota indexada",
|
||||
"notes_indexed_plural": "{{ count }} notas indexadas",
|
||||
"sources": "Fuentes",
|
||||
"start_indexing": "Comenzar indexado",
|
||||
"use_advanced_context": "Usar contexto avanzado",
|
||||
@ -2096,7 +2092,6 @@
|
||||
"read-only-info": {
|
||||
"read-only-note": "Actualmente, está viendo una nota de solo lectura.",
|
||||
"auto-read-only-note": "Esta nota se muestra en modo de solo lectura para una carga más rápida.",
|
||||
"auto-read-only-learn-more": "Para saber más",
|
||||
"edit-note": "Editar nota"
|
||||
},
|
||||
"calendar_view": {
|
||||
|
||||
@ -733,9 +733,10 @@
|
||||
"zoom_out_title": "Zoom arrière"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Lien inverse",
|
||||
"backlinks": "{{count}} Liens inverses",
|
||||
"relation": "relation"
|
||||
"relation": "relation",
|
||||
"backlink_one": "{{count}} Lien inverse",
|
||||
"backlink_many": "",
|
||||
"backlink_other": "{{count}} Liens inverses"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Insérer une note enfant",
|
||||
@ -1766,12 +1767,6 @@
|
||||
"not_started": "Non démarré",
|
||||
"title": "Paramètres IA",
|
||||
"processed_notes": "Notes traitées",
|
||||
"n_notes_queued_0": "{{ count }} note en attente d’indexation",
|
||||
"n_notes_queued_1": "{{ count }} notes en attente d’indexation",
|
||||
"n_notes_queued_2": "",
|
||||
"notes_indexed_0": "{{ count }} note indexée",
|
||||
"notes_indexed_1": "{{ count }} notes indexées",
|
||||
"notes_indexed_2": "",
|
||||
"anthropic_url_description": "URL de base pour l'API Anthropic (par défaut : https ://api.anthropic.com)",
|
||||
"anthropic_model_description": "Modèles Anthropic Claude pour la complétion",
|
||||
"voyage_settings": "Réglages d'IA Voyage",
|
||||
|
||||
@ -324,7 +324,8 @@
|
||||
"copy-link": "Copia collegamento",
|
||||
"paste-as-plain-text": "Incolla come testo semplice",
|
||||
"add-term-to-dictionary": "Aggiungi \"{{term}}\" al dizionario",
|
||||
"search_online": "Cerca \"{{term}}\" con {{searchEngine}}"
|
||||
"search_online": "Cerca \"{{term}}\" con {{searchEngine}}",
|
||||
"search_in_trilium": "Cerca \"{{term}}\" in Trilium"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
@ -614,7 +615,8 @@
|
||||
"showSQLConsole": "mostra console SQL",
|
||||
"other": "Altro",
|
||||
"quickSearch": "concentrati sull'input della ricerca rapida",
|
||||
"inPageSearch": "ricerca all'interno della pagina"
|
||||
"inPageSearch": "ricerca all'interno della pagina",
|
||||
"editShortcuts": "Modifica scorciatoie da tastiera"
|
||||
},
|
||||
"i18n": {
|
||||
"saturday": "Sabato",
|
||||
@ -638,12 +640,6 @@
|
||||
"friday": "Venerdì"
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued_0": "{{ count }} nota in coda per l'indicizzazione",
|
||||
"n_notes_queued_1": "{{ count }} note in coda per l'indicizzazione",
|
||||
"n_notes_queued_2": "{{ count }} note in coda per l'indicizzazione",
|
||||
"notes_indexed_0": "{{ count }} nota indicizzata",
|
||||
"notes_indexed_1": "{{ count }} note indicizzate",
|
||||
"notes_indexed_2": "{{ count }} note indicizzate",
|
||||
"not_started": "Non iniziato",
|
||||
"title": "Impostazioni AI",
|
||||
"processed_notes": "Note elaborate",
|
||||
@ -1308,9 +1304,10 @@
|
||||
"zoom_out_title": "Rimpicciolisci"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Backlink",
|
||||
"backlinks": "{{count}} Backlinks",
|
||||
"relation": "relazione"
|
||||
"relation": "relazione",
|
||||
"backlink_one": "{{count}} Backlink",
|
||||
"backlink_many": "{{count}} Backlinks",
|
||||
"backlink_other": "{{count}} Backlinks"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Inserisci nota secondaria",
|
||||
@ -1346,7 +1343,11 @@
|
||||
"geo-map": "Mappa geografica",
|
||||
"board": "Asse",
|
||||
"presentation": "Presentazione",
|
||||
"include_archived_notes": "Mostra note archiviate"
|
||||
"include_archived_notes": "Mostra note archiviate",
|
||||
"expand_tooltip": "Espande i figli diretti di questa raccolta (a un livello di profondità). Per ulteriori opzioni, premere la freccia a destra.",
|
||||
"expand_first_level": "Espandi figli diretti",
|
||||
"expand_nth_level": "Espandi {{depth}} livelli",
|
||||
"expand_all_levels": "Espandi tutti i livelli"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "Nessuna nota modificata per questo giorno...",
|
||||
@ -1800,8 +1801,8 @@
|
||||
"relation-map": "Mappa delle relazioni",
|
||||
"note-map": "Nota Mappa",
|
||||
"render-note": "Nota di rendering",
|
||||
"book": "Collezione",
|
||||
"mermaid-diagram": "Diagramma della sirena",
|
||||
"book": "Raccolta",
|
||||
"mermaid-diagram": "Diagramma Mermaid",
|
||||
"canvas": "Tela",
|
||||
"web-view": "Visualizzazione Web",
|
||||
"mind-map": "Mappa mentale",
|
||||
@ -1852,7 +1853,8 @@
|
||||
"refresh-saved-search-results": "Aggiorna i risultati della ricerca salvati",
|
||||
"create-child-note": "Crea nota figlio",
|
||||
"unhoist": "Sganciare",
|
||||
"toggle-sidebar": "Attiva/disattiva la barra laterale"
|
||||
"toggle-sidebar": "Attiva/disattiva la barra laterale",
|
||||
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Mantieni la finestra in primo piano"
|
||||
@ -1936,7 +1938,8 @@
|
||||
"duplicate-launcher": "Duplica il launcher <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Rilevato automaticamente"
|
||||
"auto-detect-language": "Rilevato automaticamente",
|
||||
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Blocchi di codice",
|
||||
@ -1964,7 +1967,8 @@
|
||||
"open_note_in_new_tab": "Apri la nota in una nuova scheda",
|
||||
"open_note_in_new_split": "Apri nota in una nuova divisione",
|
||||
"open_note_in_new_window": "Apri la nota in una nuova finestra",
|
||||
"open_note_in_popup": "Modifica rapida"
|
||||
"open_note_in_popup": "Modifica rapida",
|
||||
"open_note_in_other_split": "Apri nota nell'altra divisione"
|
||||
},
|
||||
"help-button": {
|
||||
"title": "Apri la pagina di aiuto pertinente"
|
||||
@ -2092,7 +2096,6 @@
|
||||
"read-only-info": {
|
||||
"read-only-note": "Stai visualizzando una nota di sola lettura.",
|
||||
"auto-read-only-note": "Questa nota viene visualizzata in modalità di sola lettura per un caricamento più rapido.",
|
||||
"auto-read-only-learn-more": "Per saperne di più",
|
||||
"edit-note": "Modifica nota"
|
||||
},
|
||||
"calendar_view": {
|
||||
@ -2102,5 +2105,8 @@
|
||||
"set-color": "Imposta colore nota",
|
||||
"set-custom-color": "Imposta colore personalizzato per le note",
|
||||
"clear-color": "Pulisci colore della nota"
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Passa all'editor completo"
|
||||
}
|
||||
}
|
||||
|
||||
@ -312,7 +312,8 @@
|
||||
"moveNoteUpDown": "ノートリストでノートを上/下に移動",
|
||||
"notSet": "未設定",
|
||||
"goUpDown": "ノートのリストで上下する",
|
||||
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する"
|
||||
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する",
|
||||
"editShortcuts": "キーボードショートカットを編集"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "ノートにインポート",
|
||||
@ -1157,7 +1158,8 @@
|
||||
"open_note_in_popup": "クイック編集",
|
||||
"open_note_in_new_tab": "新しいタブでノートを開く",
|
||||
"open_note_in_new_split": "新しく分割してノートを開く",
|
||||
"open_note_in_new_window": "新しいウィンドウでノートを開く"
|
||||
"open_note_in_new_window": "新しいウィンドウでノートを開く",
|
||||
"open_note_in_other_split": "他の分割画面でノートを開く"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"quick-edit": "クイック編集",
|
||||
@ -1212,7 +1214,8 @@
|
||||
"unhoist": "ホイスト解除",
|
||||
"saved-search-note-refreshed": "保存した検索ノートが更新されました。",
|
||||
"refresh-saved-search-results": "保存した検索結果を更新",
|
||||
"toggle-sidebar": "サイドバーを切り替え"
|
||||
"toggle-sidebar": "サイドバーを切り替え",
|
||||
"dropping-not-allowed": "この場所にノートをドロップすることはできません。"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "一括操作",
|
||||
@ -1266,7 +1269,8 @@
|
||||
"reset_launcher_confirm": "本当に「{{title}}」をリセットしますか? このノート(およびその子ノート)のすべてのデータと設定が失われ、ランチャーは元の場所に戻ります。"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自動検出"
|
||||
"auto-detect-language": "自動検出",
|
||||
"keeps-crashing": "編集コンポーネントがクラッシュし続けます。Trilium を再起動してください。問題が解決しない場合は、バグレポートの作成をご検討ください。"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "コードブロック",
|
||||
@ -1504,9 +1508,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": "高度なコンテキストを使用",
|
||||
@ -1587,9 +1589,8 @@
|
||||
"this_launcher_doesnt_define_target_note": "このランチャーはターゲットノートを定義していません。"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} バックリンク",
|
||||
"backlinks": "{{count}} バックリンク",
|
||||
"relation": "リレーション"
|
||||
"relation": "リレーション",
|
||||
"backlink_other": "{{count}} 個のバックリンク"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"delete_this_note": "このノートを削除",
|
||||
@ -2096,12 +2097,14 @@
|
||||
"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": "フルエディターに切り替え"
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,9 @@
|
||||
"edit_branch_prefix": "브랜치 접두사 편집",
|
||||
"help_on_tree_prefix": "트리 접두사에 대한 도움말",
|
||||
"prefix": "접두사: ",
|
||||
"branch_prefix_saved": "브랜치 접두사가 저장되었습니다."
|
||||
"branch_prefix_saved": "브랜치 접두사가 저장되었습니다.",
|
||||
"edit_branch_prefix_multiple": "{{count}}개의 지점 접두사 편집",
|
||||
"branch_prefix_saved_multiple": "{{count}}개의 지점에 대해 지점 접두사가 저장되었습니다."
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "대량 작업",
|
||||
|
||||
@ -16,10 +16,12 @@
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Starten widget mislukt",
|
||||
"message-unknown": "Onbekende widget kan niet gestart worden omdat:\n\n{{message}}"
|
||||
"message-unknown": "Onbekende widget kan niet gestart worden omdat:\n\n{{message}}",
|
||||
"message-custom": "Aangepaste widget van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden geïnitialiseerd vanwege:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Custom script laden mislukt"
|
||||
"title": "Custom script laden mislukt",
|
||||
"message": "Script van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@ -29,14 +31,17 @@
|
||||
"search_note": "zoek voor notitie op naam",
|
||||
"link_title_mirrors": "De link titel is hetzelfde als de notitie's huidige titel",
|
||||
"link_title": "Link titel",
|
||||
"button_add_link": "Link toevoegen"
|
||||
"button_add_link": "Link toevoegen",
|
||||
"link_title_arbitrary": "snelkoppelingsnaam kan willekeurig worden aangepast"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Bewerk branch prefix",
|
||||
"save": "Opslaan",
|
||||
"branch_prefix_saved": "Branch prefix is opgeslagen.",
|
||||
"help_on_tree_prefix": "Help bij boomvoorvoegsel",
|
||||
"prefix": "Voorvoegsel: "
|
||||
"prefix": "Voorvoegsel: ",
|
||||
"edit_branch_prefix_multiple": "Bewerk zijtakvoorvoegsel voor {{count}} zijtakken",
|
||||
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen."
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Bulk acties",
|
||||
|
||||
@ -750,9 +750,6 @@
|
||||
"indexing_stopped": "Indeksowanie zatrzymane",
|
||||
"indexing_in_progress": "Indeksowanie w trakcie...",
|
||||
"last_indexed": "Ostatnio zindeksowane",
|
||||
"n_notes_queued_0": "{{ count }} notatka zakolejkowana do indeksowania",
|
||||
"n_notes_queued_1": "{{ count }} notatek zakolejkowanych do indeksowania",
|
||||
"n_notes_queued_2": "{{ count }} notatek zakolejkowanych do indeksowania",
|
||||
"note_chat": "Czat notatki",
|
||||
"note_title": "Tytuł notatki",
|
||||
"error": "Błąd",
|
||||
@ -860,9 +857,6 @@
|
||||
"enter_message": "Wpisz swoją wiadomość...",
|
||||
"error_contacting_provider": "Błąd kontaktu z dostawcą AI. Sprawdź ustawienia i połączenie internetowe.",
|
||||
"error_generating_response": "Błąd generowania odpowiedzi AI",
|
||||
"notes_indexed_0": "{{ count }} notatka zaindeksowana",
|
||||
"notes_indexed_1": "{{ count }} notatek zaindeksowanych",
|
||||
"notes_indexed_2": "",
|
||||
"sources": "Źródła",
|
||||
"start_indexing": "Rozpocznij indeksowanie",
|
||||
"use_advanced_context": "Użyj zaawansowanego kontekstu",
|
||||
@ -1237,9 +1231,10 @@
|
||||
"zoom_out_title": "Pomniejsz"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Backlink",
|
||||
"backlinks": "{{count}} Backlinków",
|
||||
"relation": "relacja"
|
||||
"relation": "relacja",
|
||||
"backlink_one": "{{count}} Backlink",
|
||||
"backlink_few": "",
|
||||
"backlink_many": "{{count}} Backlinków"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Wstaw notatkę podrzędną",
|
||||
|
||||
@ -711,9 +711,10 @@
|
||||
"zoom_out_title": "Reduzir"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Ligação Reversa",
|
||||
"backlinks": "{{count}} Ligações Reversas",
|
||||
"relation": "relação"
|
||||
"relation": "relação",
|
||||
"backlink_one": "{{count}} Ligação Reversa",
|
||||
"backlink_many": "",
|
||||
"backlink_other": "{{count}} Ligações Reversas"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Inserir nota filha",
|
||||
@ -1234,13 +1235,7 @@
|
||||
"indexing_stopped": "Indexação interrompida",
|
||||
"indexing_in_progress": "Indexação em andamento…",
|
||||
"last_indexed": "Última Indexada",
|
||||
"n_notes_queued_0": "{{ count }} nota enfileirada para indexação",
|
||||
"n_notes_queued_1": "{{ count }} notas enfileiradas para indexação",
|
||||
"n_notes_queued_2": "{{ count }} notas enfileiradas para indexação",
|
||||
"note_chat": "Conversa de Nota",
|
||||
"notes_indexed_0": "{{ count }} nota indexada",
|
||||
"notes_indexed_1": "{{ count }} notas indexadas",
|
||||
"notes_indexed_2": "{{ count }} notas indexadas",
|
||||
"sources": "Origens",
|
||||
"start_indexing": "Iniciar Indexação",
|
||||
"use_advanced_context": "Usar Contexto Avançado",
|
||||
|
||||
@ -75,12 +75,6 @@
|
||||
"note_cloned": "A nota \"{{clonedTitle}}\" foi clonada para \"{{targetTitle}}\""
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued_0": "{{ count }} nota enfileirada para indexação",
|
||||
"n_notes_queued_1": "{{ count }} notas enfileiradas para indexação",
|
||||
"n_notes_queued_2": "{{ count }} notas enfileiradas para indexação",
|
||||
"notes_indexed_0": "{{ count }} nota indexada",
|
||||
"notes_indexed_1": "{{ count }} notas indexadas",
|
||||
"notes_indexed_2": "{{ count }} notas indexadas",
|
||||
"temperature": "Temperatura",
|
||||
"retry_queued": "Nota enfileirada para nova tentativa",
|
||||
"queued_notes": "Notas Enfileiradas",
|
||||
@ -976,9 +970,10 @@
|
||||
"reset_pan_zoom_title": "Redefinir pan & zoom para coordenadas e ampliação iniciais"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Links Reversos",
|
||||
"backlinks": "{{count}} Links Reversos",
|
||||
"relation": "relação"
|
||||
"relation": "relação",
|
||||
"backlink_one": "",
|
||||
"backlink_many": "",
|
||||
"backlink_other": "{{count}} Links Reversos"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Inserir nota filha",
|
||||
|
||||
@ -1362,8 +1362,9 @@
|
||||
"title": "Factorul de zoom (doar pentru versiunea desktop)"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} legături de retur",
|
||||
"backlinks": "{{count}} legături de retur",
|
||||
"backlink_one": "{{count}} legătură de retur",
|
||||
"backlink_few": "{{count}} legături de retur",
|
||||
"backlink_other": "{{count}} de legături de retur",
|
||||
"relation": "relație"
|
||||
},
|
||||
"svg_export_button": {
|
||||
@ -1887,13 +1888,7 @@
|
||||
"indexing_stopped": "Indexarea s-a oprit",
|
||||
"indexing_in_progress": "Indexare în curs...",
|
||||
"last_indexed": "Ultima indexare",
|
||||
"n_notes_queued_0": "O notiță adăugată în coada de indexare",
|
||||
"n_notes_queued_1": "{{ count }} notițe adăugate în coada de indexare",
|
||||
"n_notes_queued_2": "{{ count }} de notițe adăugate în coada de indexare",
|
||||
"note_chat": "Discuție pe baza notițelor",
|
||||
"notes_indexed_0": "O notiță indexată",
|
||||
"notes_indexed_1": "{{ count }} notițe indexate",
|
||||
"notes_indexed_2": "{{ count }} de notițe indexate",
|
||||
"sources": "Surse",
|
||||
"start_indexing": "Indexează",
|
||||
"use_advanced_context": "Folosește context îmbogățit",
|
||||
@ -2097,7 +2092,6 @@
|
||||
"read-only-info": {
|
||||
"read-only-note": "Vizualizați o notiță în modul doar în citire.",
|
||||
"auto-read-only-note": "Această notiță este afișată în modul doar în citire din motive de performanță.",
|
||||
"auto-read-only-learn-more": "Mai multe detalii",
|
||||
"edit-note": "Editează notița"
|
||||
},
|
||||
"calendar_view": {
|
||||
|
||||
@ -984,9 +984,10 @@
|
||||
"new-version-available": "Доступно обновление"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} ссылки",
|
||||
"backlinks": "{{count}} ссылок",
|
||||
"relation": "отношение"
|
||||
"relation": "отношение",
|
||||
"backlink_one": "{{count}} ссылки",
|
||||
"backlink_few": "",
|
||||
"backlink_many": "{{count}} ссылок"
|
||||
},
|
||||
"note_icon": {
|
||||
"category": "Категория:",
|
||||
@ -1334,16 +1335,10 @@
|
||||
"error_fetching": "Ошибка получения списка моделей: {{error}}",
|
||||
"index_rebuild_status_error": "Ошибка проверки статуса перестроения индекса",
|
||||
"enhanced_context_description": "Предоставляет ИИ больше контекста из заметки и связанных с ней заметок для более точных ответов",
|
||||
"n_notes_queued_0": "{{ count }} заметка в очереди на индексирование",
|
||||
"n_notes_queued_1": "{{ count }} заметки в очереди на индексирование",
|
||||
"n_notes_queued_2": "{{ count }} заметок в очереди на индексирование",
|
||||
"no_models_found_ollama": "Модели Ollama не найдены. Проверьте, запущена ли Ollama.",
|
||||
"no_models_found_online": "Модели не найдены. Проверьте ваш ключ API и настройки.",
|
||||
"experimental_warning": "Функция LLM в настоящее время является экспериментальной — вы предупреждены.",
|
||||
"ollama_no_url": "Ollama не настроена. Введите корректный URL-адрес.",
|
||||
"notes_indexed_0": "{{ count }} заметка проиндексирована",
|
||||
"notes_indexed_1": "{{ count }} заметки проиндексировано",
|
||||
"notes_indexed_2": "{{ count }} заметок проиндексировано",
|
||||
"show_thinking_description": "Показать цепочку мыслительного процесса ИИ",
|
||||
"api_key_tooltip": "API-ключ для доступа к сервису",
|
||||
"all_notes_queued_for_retry": "Все неудачные заметки поставлены в очередь на повторную попытку",
|
||||
|
||||
@ -425,14 +425,6 @@
|
||||
"print_page_size": "Prilikom izvoza u PDF, menja veličinu stranice. Podržane vrednosti: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
|
||||
"color_type": "Boja"
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued_0": "{{ count }} beleška stavljena u red za indeksiranje",
|
||||
"n_notes_queued_1": "{{ count }} beleški stavljeno u red za indeksiranje",
|
||||
"n_notes_queued_2": "{{ count }} beleški stavljeno u red za indeksiranje",
|
||||
"notes_indexed_0": "{{ count }} beleška je indeksirana",
|
||||
"notes_indexed_1": "{{ count }} beleški je indeksirano",
|
||||
"notes_indexed_2": "{{ count }} beleški je indeksirano"
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "Da biste dodali oznaku, samo unesite npr. <code>#rock</code> ili ako želite da dodate i vrednost, onda npr. <code>#year = 2020</code>",
|
||||
"help_text_body2": "Za relaciju, unesite <code>~author = @</code> što bi trebalo da otvori automatsko dovršavanje gde možete potražiti željenu belešku.",
|
||||
|
||||
@ -96,11 +96,5 @@
|
||||
"are_you_sure_remove_note": "\"{{title}}\" notunu ilişki haritasından kaldırmak istediğinize emin misiniz?. ",
|
||||
"also_delete_note": "Notu da sil",
|
||||
"if_you_dont_check": "Bunu işaretlemezseniz, not yalnızca ilişki haritasından kaldırılacaktır."
|
||||
},
|
||||
"ai_llm": {
|
||||
"n_notes_queued": "{{ count }} not dizinleme için sıraya alındı",
|
||||
"n_notes_queued_plural": "{{ count }} adet not dizinleme için sıraya alındı",
|
||||
"notes_indexed": "{{ count }} not dizinlendi",
|
||||
"notes_indexed_plural": "{{ count }} adet not dizinlendi"
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +162,8 @@
|
||||
"inPageSearch": "頁面內搜尋",
|
||||
"title": "列表",
|
||||
"newTabNoteLink": "在新分頁開啟筆記連結",
|
||||
"newTabWithActivationNoteLink": "在新分頁開啟並切換至筆記連結"
|
||||
"newTabWithActivationNoteLink": "在新分頁開啟並切換至筆記連結",
|
||||
"editShortcuts": "編輯鍵盤快捷鍵"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "匯入至筆記",
|
||||
@ -732,9 +733,8 @@
|
||||
"zoom_out_title": "縮小"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} 個反連結",
|
||||
"backlinks": "{{count}} 個反連結",
|
||||
"relation": "關聯"
|
||||
"relation": "關聯",
|
||||
"backlink_one": "{{count}} 個反連結"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "插入子筆記",
|
||||
@ -1516,7 +1516,8 @@
|
||||
"refresh-saved-search-results": "重新整理儲存的搜尋結果",
|
||||
"create-child-note": "建立子筆記",
|
||||
"unhoist": "取消聚焦",
|
||||
"toggle-sidebar": "切換側邊欄"
|
||||
"toggle-sidebar": "切換側邊欄",
|
||||
"dropping-not-allowed": "不允許移動筆記至此處。"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此視窗置頂"
|
||||
@ -1619,7 +1620,8 @@
|
||||
"duplicate-launcher": "複製啟動器 <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自動檢測"
|
||||
"auto-detect-language": "自動檢測",
|
||||
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在,請考慮提交錯誤報告。"
|
||||
},
|
||||
"highlighting": {
|
||||
"description": "控制文字筆記程式碼區塊中的語法高亮,程式碼筆記不會受到影響。",
|
||||
@ -1795,9 +1797,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": "使用進階上下文",
|
||||
@ -2096,12 +2096,14 @@
|
||||
"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": "切換至完整編輯器"
|
||||
}
|
||||
}
|
||||
|
||||
@ -839,9 +839,10 @@
|
||||
"zoom_out_title": "Зменшити масштаб"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} Зворотне посилання",
|
||||
"backlinks": "{{count}} Зворотні посилання",
|
||||
"relation": "зв'язок"
|
||||
"relation": "зв'язок",
|
||||
"backlink_one": "{{count}} Зворотне посилання",
|
||||
"backlink_few": "{{count}} Зворотні посилання",
|
||||
"backlink_many": "{{count}} Зворотні посилання"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Вставити дочірню нотатку",
|
||||
@ -1350,13 +1351,7 @@
|
||||
"indexing_stopped": "Індексацію зупинено",
|
||||
"indexing_in_progress": "Триває індексація...",
|
||||
"last_indexed": "Остання індексація",
|
||||
"n_notes_queued_0": "{{ count }} нотатка в черзі на індексацію",
|
||||
"n_notes_queued_1": "{{ count }} нотатки в черзі на індексацію",
|
||||
"n_notes_queued_2": "{{ count }} нотаток в черзі на індексацію",
|
||||
"note_chat": "Нотатка Чат",
|
||||
"notes_indexed_0": "{{ count }} нотатка індексовано",
|
||||
"notes_indexed_1": "{{ count }} нотатки індексовано",
|
||||
"notes_indexed_2": "{{ count }} нотаток індексовано",
|
||||
"sources": "Джерела",
|
||||
"start_indexing": "Почати індексацію",
|
||||
"use_advanced_context": "Використовувати розширений контекст",
|
||||
|
||||
@ -18,6 +18,8 @@ import froca from "../services/froca";
|
||||
import NoteLink from "./react/NoteLink";
|
||||
import RawHtml from "./react/RawHtml";
|
||||
import { ViewTypeOptions } from "./collections/interface";
|
||||
import attributes from "../services/attributes";
|
||||
import LoadResults from "../services/load_results";
|
||||
|
||||
export interface FloatingButtonContext {
|
||||
parentComponent: Component;
|
||||
@ -310,13 +312,18 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
let [ popupOpen, setPopupOpen ] = useState(false);
|
||||
const backlinksContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function refresh() {
|
||||
if (!isDefaultViewMode) return;
|
||||
|
||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||
setBacklinkCount(resp.count);
|
||||
});
|
||||
}, [ note ]);
|
||||
}
|
||||
|
||||
useEffect(() => refresh(), [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (needsRefresh(note, loadResults)) refresh();
|
||||
});
|
||||
|
||||
// Determine the max height of the container.
|
||||
const { windowHeight } = useWindowSize();
|
||||
@ -341,18 +348,18 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
|
||||
{popupOpen && (
|
||||
<div ref={backlinksContainerRef} className="backlinks-items dropdown-menu" style={{ display: "block" }}>
|
||||
<BacklinksList noteId={note.noteId} />
|
||||
<BacklinksList note={note} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksList({ noteId }: { noteId: string }) {
|
||||
function BacklinksList({ note }: { note: FNote }) {
|
||||
const [ backlinks, setBacklinks ] = useState<BacklinksResponse>([]);
|
||||
|
||||
useEffect(() => {
|
||||
server.get<BacklinksResponse>(`note-map/${noteId}/backlinks`).then(async (backlinks) => {
|
||||
function refresh() {
|
||||
server.get<BacklinksResponse>(`note-map/${note.noteId}/backlinks`).then(async (backlinks) => {
|
||||
// prefetch all
|
||||
const noteIds = backlinks
|
||||
.filter(bl => "noteId" in bl)
|
||||
@ -360,7 +367,12 @@ function BacklinksList({ noteId }: { noteId: string }) {
|
||||
await froca.getNotes(noteIds);
|
||||
setBacklinks(backlinks);
|
||||
});
|
||||
}, [ noteId ]);
|
||||
}
|
||||
|
||||
useEffect(() => refresh(), [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (needsRefresh(note, loadResults)) refresh();
|
||||
});
|
||||
|
||||
return backlinks.map(backlink => (
|
||||
<div>
|
||||
@ -380,3 +392,9 @@ function BacklinksList({ noteId }: { noteId: string }) {
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
function needsRefresh(note: FNote, loadResults: LoadResults) {
|
||||
return loadResults.getAttributeRows().some(attr =>
|
||||
attr.type === "relation" &&
|
||||
attributes.isAffecting(attr, note));
|
||||
}
|
||||
|
||||
@ -28,8 +28,9 @@ export default function NoteDetail() {
|
||||
const { note, type, mime, noteContext, parentComponent } = useNoteInfo();
|
||||
const { ntxId, viewScope } = noteContext ?? {};
|
||||
const isFullHeight = checkFullHeight(noteContext, type);
|
||||
const noteTypesToRender = useRef<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
|
||||
const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
|
||||
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
|
||||
const widgetRequestId = useRef(0);
|
||||
|
||||
const props: TypeWidgetProps = {
|
||||
note: note!,
|
||||
@ -38,19 +39,28 @@ export default function NoteDetail() {
|
||||
parentComponent,
|
||||
noteContext
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!type) return;
|
||||
const requestId = ++widgetRequestId.current;
|
||||
|
||||
if (!noteTypesToRender.current[type]) {
|
||||
if (!noteTypesToRender[type]) {
|
||||
getCorrespondingWidget(type).then((el) => {
|
||||
if (!el) return;
|
||||
noteTypesToRender.current[type] = el;
|
||||
|
||||
// Ignore stale requests
|
||||
if (requestId !== widgetRequestId.current) return;
|
||||
|
||||
setNoteTypesToRender(prev => ({
|
||||
...prev,
|
||||
[type]: el
|
||||
}));
|
||||
setActiveNoteType(type);
|
||||
});
|
||||
} else {
|
||||
setActiveNoteType(type);
|
||||
}
|
||||
}, [ note, viewScope, type ]);
|
||||
}, [ note, viewScope, type, noteTypesToRender ]);
|
||||
|
||||
// Detect note type changes.
|
||||
useTriliumEvent("entitiesReloaded", async ({ loadResults }) => {
|
||||
@ -95,9 +105,11 @@ export default function NoteDetail() {
|
||||
});
|
||||
|
||||
// Automatically focus the editor.
|
||||
useTriliumEvent("activeNoteChanged", () => {
|
||||
// Restore focus to the editor when switching tabs, but only if the note tree is not already focused.
|
||||
if (!document.activeElement?.classList.contains("fancytree-title")) {
|
||||
useTriliumEvent("activeNoteChanged", ({ ntxId: eventNtxId }) => {
|
||||
if (eventNtxId != ntxId) return;
|
||||
// Restore focus to the editor when switching tabs,
|
||||
// but only if the note tree and the note panel (e.g., note title or note detail) are not focused.
|
||||
if (!document.activeElement?.classList.contains("fancytree-title") && !parentComponent.$widget[0].closest(".note-split")?.contains(document.activeElement)) {
|
||||
parentComponent.triggerCommand("focusOnDetail", { ntxId });
|
||||
}
|
||||
});
|
||||
@ -192,7 +204,7 @@ export default function NoteDetail() {
|
||||
ref={containerRef}
|
||||
class={`note-detail ${isFullHeight ? "full-height" : ""}`}
|
||||
>
|
||||
{Object.entries(noteTypesToRender.current).map(([ itemType, Element ]) => {
|
||||
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
|
||||
return <NoteDetailWrapper
|
||||
Element={Element}
|
||||
key={itemType}
|
||||
|
||||
@ -14,8 +14,10 @@ import ws from "../services/ws";
|
||||
import { UpdateAttributeResponse } from "@triliumnext/commons";
|
||||
import attributes from "../services/attributes";
|
||||
import debounce from "../services/debounce";
|
||||
import { randomString } from "../services/utils";
|
||||
|
||||
interface Cell {
|
||||
uniqueId: string;
|
||||
definitionAttr: FAttribute;
|
||||
definition: DefinitionObject;
|
||||
valueAttr: Attribute;
|
||||
@ -44,6 +46,7 @@ export default function PromotedAttributes() {
|
||||
<div className="promoted-attributes-widget">
|
||||
{cells && cells.length > 0 && <div className="promoted-attributes-container">
|
||||
{note && cells?.map(cell => <PromotedAttributeCell
|
||||
key={cell.uniqueId}
|
||||
cell={cell}
|
||||
cells={cells} setCells={setCells}
|
||||
shouldFocus={cell === cellToFocus} setCellToFocus={setCellToFocus}
|
||||
@ -103,7 +106,8 @@ function usePromotedAttributeData(note: FNote | null | undefined, componentId: s
|
||||
valueAttr.attributeId = "";
|
||||
}
|
||||
|
||||
cells.push({ definitionAttr, definition, valueAttr, valueName });
|
||||
const uniqueId = randomString(10);
|
||||
cells.push({ definitionAttr, definition, valueAttr, valueName, uniqueId });
|
||||
}
|
||||
}
|
||||
setCells(cells);
|
||||
|
||||
@ -3,28 +3,27 @@ import { t } from "../services/i18n";
|
||||
import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks"
|
||||
import Button from "./react/Button";
|
||||
import InfoBar from "./react/InfoBar";
|
||||
import HelpButton from "./react/HelpButton";
|
||||
|
||||
export default function ReadOnlyNoteInfoBar(props: {}) {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
||||
|
||||
return <InfoBar className="read-only-note-info-bar-widget"
|
||||
return (
|
||||
<InfoBar
|
||||
className="read-only-note-info-bar-widget"
|
||||
type={(isExplicitReadOnly ? "subtle" : "prominent")}
|
||||
style={{display: (!isReadOnly) ? "none" : undefined}}>
|
||||
|
||||
style={{display: (!isReadOnly) ? "none" : undefined}}
|
||||
>
|
||||
<div class="read-only-note-info-bar-widget-content">
|
||||
{(isExplicitReadOnly) ? (
|
||||
<div>{t("read-only-info.read-only-note")}</div>
|
||||
) : (
|
||||
<div>
|
||||
{t("read-only-info.auto-read-only-note")}
|
||||
|
||||
<a class="tn-link"
|
||||
href="https://docs.triliumnotes.org/user-guide/concepts/notes/read-only-notes#automatic-read-only-mode">
|
||||
|
||||
{t("read-only-info.auto-read-only-learn-more")}
|
||||
</a>
|
||||
{" "}
|
||||
<HelpButton helpPage="CoFPLs3dRlXc" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -32,5 +31,5 @@ export default function ReadOnlyNoteInfoBar(props: {}) {
|
||||
icon="bx-pencil" onClick={() => enableEditing()} />
|
||||
</div>
|
||||
</InfoBar>
|
||||
|
||||
);
|
||||
}
|
||||
@ -110,7 +110,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
private weekNotes: string[] = [];
|
||||
|
||||
constructor(title: string = "", icon: string = "") {
|
||||
super(title, icon, DROPDOWN_TPL);
|
||||
super(title, icon, DROPDOWN_TPL, "calendar-dropdown-menu");
|
||||
}
|
||||
|
||||
doRender() {
|
||||
@ -211,8 +211,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
const $target = $(e.target);
|
||||
|
||||
// Keep dropdown open when clicking on month select button or year selector area
|
||||
if ($target.closest('.btn.dropdown-toggle.select-button').length ||
|
||||
$target.closest('.calendar-year-selector').length) {
|
||||
if ($target.closest('.btn.dropdown-toggle.select-button').length) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
const isVerticalLayout = !isHorizontalLayout;
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||
const isMobileLocal = isMobile();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@ -38,6 +39,8 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
</div>}
|
||||
</>}
|
||||
noDropdownListStyle
|
||||
onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined}
|
||||
onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined}
|
||||
>
|
||||
|
||||
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||
|
||||
@ -7,9 +7,9 @@ const TPL = /*html*/`
|
||||
<div class="dropdown right-dropdown-widget">
|
||||
<button type="button" data-bs-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false"
|
||||
class="bx right-dropdown-button launcher-button"></button>
|
||||
|
||||
class="bx right-dropdown-button launcher-button">
|
||||
<div class="tooltip-trigger"></div>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu"></div>
|
||||
</div>
|
||||
@ -24,14 +24,16 @@ export default class RightDropdownButtonWidget extends BasicWidget {
|
||||
protected dropdown!: Dropdown;
|
||||
protected $tooltip!: JQuery<HTMLElement>;
|
||||
protected tooltip!: Tooltip;
|
||||
private dropdownClass?: string;
|
||||
public $dropdownContent!: JQuery<HTMLElement>;
|
||||
|
||||
constructor(title: string, iconClass: string, dropdownTpl: string) {
|
||||
constructor(title: string, iconClass: string, dropdownTpl: string, dropdownClass?: string) {
|
||||
super();
|
||||
|
||||
this.iconClass = iconClass;
|
||||
this.title = title;
|
||||
this.dropdownTpl = dropdownTpl;
|
||||
this.dropdownClass = dropdownClass;
|
||||
|
||||
this.settings = {
|
||||
titlePlacement: "right"
|
||||
@ -41,15 +43,17 @@ export default class RightDropdownButtonWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$dropdownMenu = this.$widget.find(".dropdown-menu");
|
||||
if (this.dropdownClass) {
|
||||
this.$dropdownMenu.addClass(this.dropdownClass);
|
||||
}
|
||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
|
||||
popperConfig: {
|
||||
placement: this.settings.titlePlacement,
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.attr("title", this.title);
|
||||
this.tooltip = Tooltip.getOrCreateInstance(this.$widget[0], {
|
||||
trigger: "hover",
|
||||
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
|
||||
this.tooltip = new Tooltip(this.$tooltip[0], {
|
||||
placement: handleRightToLeftPlacement(this.settings.titlePlacement),
|
||||
fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ]
|
||||
});
|
||||
@ -57,7 +61,9 @@ export default class RightDropdownButtonWidget extends BasicWidget {
|
||||
this.$widget
|
||||
.find(".right-dropdown-button")
|
||||
.addClass(this.iconClass)
|
||||
.on("click", () => this.tooltip.hide());
|
||||
.on("click", () => this.tooltip.hide())
|
||||
.on("mouseenter", () => this.tooltip.show())
|
||||
.on("mouseleave", () => this.tooltip.hide());
|
||||
|
||||
this.$widget.on("show.bs.dropdown", async () => {
|
||||
await this.dropdownShown();
|
||||
|
||||
@ -2,9 +2,9 @@ import FNote from "../../../entities/fnote";
|
||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||
import link_context_menu from "../../../menus/link_context_menu";
|
||||
import attributes from "../../../services/attributes";
|
||||
import branches from "../../../services/branches";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||
import { t } from "../../../services/i18n";
|
||||
import Api from "./api";
|
||||
|
||||
@ -41,18 +41,7 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
||||
x: event.pageX,
|
||||
y: event.pageY,
|
||||
items: [
|
||||
...link_context_menu.getItems(),
|
||||
{ kind: "separator" },
|
||||
{
|
||||
title: t("board_view.move-to"),
|
||||
uiIcon: "bx bx-transfer",
|
||||
items: api.columns.map(columnToMoveTo => ({
|
||||
title: columnToMoveTo,
|
||||
enabled: columnToMoveTo !== column,
|
||||
handler: () => api.changeColumn(note.noteId, columnToMoveTo)
|
||||
})),
|
||||
},
|
||||
getArchiveMenuItem(note),
|
||||
...link_context_menu.getItems(event),
|
||||
{ kind: "separator" },
|
||||
{
|
||||
title: t("board_view.insert-above"),
|
||||
@ -65,6 +54,17 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
||||
handler: () => api.insertRowAtPosition(column, branchId, "after")
|
||||
},
|
||||
{ kind: "separator" },
|
||||
{
|
||||
title: t("board_view.move-to"),
|
||||
uiIcon: "bx bx-transfer",
|
||||
items: api.columns.map(columnToMoveTo => ({
|
||||
title: columnToMoveTo,
|
||||
enabled: columnToMoveTo !== column,
|
||||
handler: () => api.changeColumn(note.noteId, columnToMoveTo)
|
||||
})),
|
||||
},
|
||||
{ kind: "separator" },
|
||||
getArchiveMenuItem(note),
|
||||
{
|
||||
title: t("board_view.remove-from-board"),
|
||||
uiIcon: "bx bx-task-x",
|
||||
@ -81,24 +81,7 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
||||
componentFn: () => NoteColorPicker({note})
|
||||
}
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, note.noteId),
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, event, note.noteId),
|
||||
});
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,8 +204,19 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
|
||||
setIsCreatingNewColumn(true);
|
||||
}, []);
|
||||
|
||||
const keydownCallback = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
setIsCreatingNewColumn(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`board-add-column ${isCreatingNewColumn ? "editing" : ""}`} onClick={addColumnCallback}>
|
||||
<div
|
||||
className={`board-add-column ${isCreatingNewColumn ? "editing" : ""}`}
|
||||
onClick={addColumnCallback}
|
||||
onKeyDown={keydownCallback}
|
||||
tabIndex={300}
|
||||
>
|
||||
{!isCreatingNewColumn
|
||||
? <>
|
||||
<Icon icon="bx bx-plus" />{" "}
|
||||
|
||||
@ -3,11 +3,10 @@ import FNote from "../../../entities/fnote";
|
||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||
import link_context_menu from "../../../menus/link_context_menu";
|
||||
import branches from "../../../services/branches";
|
||||
import froca from "../../../services/froca";
|
||||
import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js";
|
||||
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||
import { t } from "../../../services/i18n";
|
||||
|
||||
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
||||
export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parentNote: FNote) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@ -15,17 +14,15 @@ export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, par
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
...link_context_menu.getItems(),
|
||||
...link_context_menu.getItems(e),
|
||||
{ kind: "separator" },
|
||||
getArchiveMenuItem(note),
|
||||
{
|
||||
title: t("calendar_view.delete_note"),
|
||||
uiIcon: "bx bx-trash",
|
||||
handler: async () => {
|
||||
const noteToDelete = await froca.getNote(noteId);
|
||||
if (!noteToDelete) return;
|
||||
|
||||
let branchIdToDelete: string | null = null;
|
||||
for (const parentBranch of noteToDelete.getParentBranches()) {
|
||||
for (const parentBranch of note.getParentBranches()) {
|
||||
const parentNote = await parentBranch.getNote();
|
||||
if (parentNote?.hasAncestor(parentNote.noteId)) {
|
||||
branchIdToDelete = parentBranch.branchId;
|
||||
@ -40,9 +37,9 @@ export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, par
|
||||
{ kind: "separator" },
|
||||
{
|
||||
kind: "custom",
|
||||
componentFn: () => NoteColorPicker({note: noteId})
|
||||
componentFn: () => NoteColorPicker({note: note})
|
||||
}
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, e, note.noteId),
|
||||
})
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import froca from "../../../services/froca";
|
||||
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import server from "../../../services/server";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface Event {
|
||||
startDate: string,
|
||||
@ -80,7 +81,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg)
|
||||
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) {
|
||||
const customTitleAttributeName = note.getLabelValue("calendar:title");
|
||||
const titles = await parseCustomTitle(customTitleAttributeName, note);
|
||||
const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color");
|
||||
const colorClass = note.getColorClass();
|
||||
const events: EventInput[] = [];
|
||||
|
||||
const calendarDisplayedAttributes = note.getLabelValue("calendar:displayedAttributes")?.split(",");
|
||||
@ -108,10 +109,9 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
|
||||
start: startDate,
|
||||
url: `#${note.noteId}?popup`,
|
||||
noteId: note.noteId,
|
||||
color: color ?? undefined,
|
||||
iconClass: note.getLabelValue("iconClass"),
|
||||
promotedAttributes: displayedAttributesData,
|
||||
className: isArchived ? "archived" : ""
|
||||
className: clsx({archived: isArchived}, colorClass)
|
||||
};
|
||||
if (endDate) {
|
||||
eventData.end = endDate;
|
||||
|
||||
@ -1,4 +1,21 @@
|
||||
:root {
|
||||
/* Default values to be overridden by themes */
|
||||
--calendar-coll-event-background-lightness: 95%;
|
||||
--calendar-coll-event-background-saturation: 80%;
|
||||
--calendar-coll-event-background-color: var(--accented-background-color);
|
||||
--calendar-coll-event-text-color: var(--main-text-color);
|
||||
--calendar-coll-event-hover-filter: none;
|
||||
--callendar-coll-event-archived-sripe-color: #00000013;
|
||||
--calendar-coll-today-background-color: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.calendar-view {
|
||||
--fc-event-border-color: var(--calendar-coll-event-text-color);
|
||||
--fc-event-bg-color: var(--calendar-coll-event-background-color);
|
||||
--fc-event-text-color: var(--calendar-coll-event-text-color);
|
||||
--fc-event-selected-overlay-color: transparent;
|
||||
--fc-today-bg-color: var(--calendar-coll-today-background-color);
|
||||
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
@ -7,8 +24,9 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.calendar-view a {
|
||||
color: unset;
|
||||
.calendar-view a,
|
||||
:root .calendar-view a.fc-daygrid-event:hover {
|
||||
color: var(--fc-event-text-color);
|
||||
}
|
||||
|
||||
.search-result-widget-content .calendar-view {
|
||||
@ -31,14 +49,6 @@
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.calendar-container a.fc-event {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.calendar-container a.fc-event.archived {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.calendar-container .fc-button {
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
@ -76,3 +86,106 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
|
||||
padding-inline-end: unset !important;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Events */
|
||||
|
||||
/*
|
||||
* week, month, year views
|
||||
*/
|
||||
|
||||
.calendar-container a.fc-event {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.calendar-container a.fc-event.archived {
|
||||
opacity: .65;
|
||||
}
|
||||
|
||||
.calendar-container a.fc-event.archived::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
|
||||
--c1: transparent;
|
||||
--c2: var(--callendar-coll-event-archived-sripe-color);
|
||||
|
||||
background: repeating-linear-gradient(45deg, var(--c1), var(--c1) 8px,
|
||||
var(--c2) 8px, var(--c2) 16px);
|
||||
}
|
||||
|
||||
.calendar-view a.fc-timegrid-event,
|
||||
.calendar-view a.fc-daygrid-event,
|
||||
.calendar-view .fc-daygrid-dot-event .fc-event-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-view a.fc-timegrid-event,
|
||||
.calendar-view a.fc-daygrid-event {
|
||||
--border-color: transparent;
|
||||
|
||||
border: 2px solid;
|
||||
border-left-width: 4px;
|
||||
border-color: var(--border-color) var(--border-color) var(--border-color)
|
||||
var(--fc-event-text-color) !important;
|
||||
background: var(--fc-event-bg-color) !important;
|
||||
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.calendar-view .fc-timegrid-event.with-hue,
|
||||
.calendar-view .fc-daygrid-event.with-hue {
|
||||
--fc-event-text-color: var(--custom-color);
|
||||
|
||||
--fc-event-bg-color: hsl(var(--custom-color-hue),
|
||||
var(--calendar-coll-event-background-saturation),
|
||||
var(--calendar-coll-event-background-lightness)) !important;
|
||||
}
|
||||
|
||||
.calendar-view a.fc-timegrid-event:focus-visible,
|
||||
.calendar-view a.fc-daygrid-event:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.calendar-view a.fc-timegrid-event.fc-event-selected,
|
||||
.calendar-view a.fc-timegrid-event.fc-event:focus,
|
||||
.calendar-view a.fc-daygrid-event.fc-event-selected,
|
||||
.calendar-view a.fc-daygrid-event.fc-event:focus {
|
||||
--border-color: var(--custom-color, var(--input-focus-outline-color));
|
||||
}
|
||||
|
||||
.calendar-view a.fc-timegrid-event:hover,
|
||||
.calendar-view a.fc-daygrid-event:hover {
|
||||
filter: var(--calendar-coll-event-hover-filter);
|
||||
border-color: var(--fc-event-text-color);
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.calendar-view .fc-daygrid-event-dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.calendar-view .fc-event-time {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
/*
|
||||
* List view
|
||||
*/
|
||||
|
||||
.fc-list-table tr.fc-event.archived {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.fc-list-table .fc-list-event-dot {
|
||||
/* Apply note colors to the list item dots */
|
||||
--fc-event-border-color: var(--custom-color);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@ -77,6 +77,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
|
||||
uk: () => import("@fullcalendar/core/locales/uk"),
|
||||
en: null,
|
||||
"en-GB": () => import("@fullcalendar/core/locales/en-gb"),
|
||||
"en_rtl": null,
|
||||
ar: () => import("@fullcalendar/core/locales/ar")
|
||||
};
|
||||
@ -116,7 +117,10 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
if (loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) // note title change.
|
||||
|| loadResults.getAttributeRows().some((a) => noteIds.includes(a.noteId ?? ""))) // subnote change.
|
||||
{
|
||||
// Defer execution after the load results are processed so that the event builder has the updated data to work with.
|
||||
setTimeout(() => {
|
||||
calendarRef.current?.refetchEvents();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
@ -306,9 +310,11 @@ function useEventDisplayCustomization(parentNote: FNote) {
|
||||
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
||||
}
|
||||
|
||||
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
|
||||
const noteId = e.event.extendedProps.noteId;
|
||||
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
|
||||
e.el.addEventListener("contextmenu", async (contextMenuEvent) => {
|
||||
const note = await froca.getNote(e.event.extendedProps.noteId);
|
||||
if (!note) return;
|
||||
|
||||
openCalendarContextMenu(contextMenuEvent, note, parentNote);
|
||||
});
|
||||
}, []);
|
||||
return { eventDidMount };
|
||||
|
||||
@ -12,7 +12,7 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
|
||||
let items: MenuItem<keyof CommandMappings>[] = [
|
||||
...buildGeoLocationItem(e),
|
||||
{ kind: "separator" },
|
||||
...linkContextMenu.getItems(),
|
||||
...linkContextMenu.getItems(e),
|
||||
];
|
||||
|
||||
if (isEditable) {
|
||||
@ -32,14 +32,14 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
|
||||
x: e.originalEvent.pageX,
|
||||
y: e.originalEvent.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: ({ command }, e) => {
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command === "deleteFromMap") {
|
||||
appContext.triggerCommand(command, { noteId });
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the events to the link context menu
|
||||
linkContextMenu.handleLinkContextMenuItem(command, noteId);
|
||||
linkContextMenu.handleLinkContextMenuItem(command, e, noteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
||||
|
||||
contextMenu.show({
|
||||
items: [
|
||||
...link_context_menu.getItems(),
|
||||
...link_context_menu.getItems(e),
|
||||
{ kind: "separator" },
|
||||
{
|
||||
title: t("table_view.row-insert-above"),
|
||||
@ -227,7 +227,7 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
||||
componentFn: () => NoteColorPicker({note: rowData.noteId})
|
||||
}
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, rowData.noteId),
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, e, rowData.noteId),
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
});
|
||||
|
||||
@ -29,7 +29,11 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
|
||||
if (visible) {
|
||||
this.triggerEvent("focusTree", {});
|
||||
} else {
|
||||
this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId });
|
||||
const ntxId = appContext.tabManager.getActiveContext()?.ntxId;
|
||||
const noteContainer = document.querySelector(`.note-split[data-ntx-id="${ntxId}"]`);
|
||||
if (!noteContainer?.contains(document.activeElement)) {
|
||||
this.triggerEvent("focusOnDetail", { ntxId });
|
||||
}
|
||||
}
|
||||
|
||||
options.save("leftPaneVisible", this.currentLeftPaneVisible.toString());
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import type NoteContext from "../../components/note_context.js";
|
||||
import Component from "../../components/component.js";
|
||||
import splitService from "../../services/resizer.js";
|
||||
interface NoteContextEvent {
|
||||
noteContext: NoteContext;
|
||||
}
|
||||
import { isMobile } from "../../services/utils.js";
|
||||
import NoteContext from "../../components/note_context.js";
|
||||
|
||||
interface SplitNoteWidget extends BasicWidget {
|
||||
hasBeenAlreadyShown?: boolean;
|
||||
@ -53,13 +51,14 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
|
||||
this.child(widget);
|
||||
|
||||
if (noteContext.mainNtxId && noteContext.ntxId) {
|
||||
if (noteContext.mainNtxId && noteContext.ntxId && !isMobile()) {
|
||||
splitService.setupNoteSplitResizer([noteContext.mainNtxId,noteContext.ntxId]);
|
||||
}
|
||||
}
|
||||
|
||||
async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: EventData<"openNewNoteSplit">) {
|
||||
const mainNtxId = appContext.tabManager.getActiveMainContext()?.ntxId;
|
||||
const activeContext = appContext.tabManager.getActiveMainContext();
|
||||
const mainNtxId = activeContext?.ntxId;
|
||||
if (!mainNtxId) {
|
||||
console.warn("Missing main note context ID");
|
||||
return;
|
||||
@ -73,22 +72,30 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
|
||||
hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
|
||||
const noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId);
|
||||
|
||||
const subContexts = activeContext.getSubContexts();
|
||||
let noteContext: NoteContext | undefined = undefined;
|
||||
if (isMobile() && subContexts.length > 1) {
|
||||
noteContext = subContexts.find(s => s.ntxId !== ntxId);
|
||||
}
|
||||
if (!noteContext) {
|
||||
noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId);
|
||||
// remove the original position of newly created note context
|
||||
const ntxIds = appContext.tabManager.children.map((c) => c.ntxId).filter((id) => id !== noteContext?.ntxId) as string[];
|
||||
|
||||
// insert the note context after the originating note context
|
||||
if (!noteContext.ntxId) {
|
||||
logError("Failed to create new note context!");
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the original position of newly created note context
|
||||
const ntxIds = appContext.tabManager.children.map((c) => c.ntxId).filter((id) => id !== noteContext.ntxId) as string[];
|
||||
|
||||
// insert the note context after the originating note context
|
||||
ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId);
|
||||
|
||||
this.triggerCommand("noteContextReorder", { ntxIdsInOrder: ntxIds });
|
||||
|
||||
// move the note context rendered widget after the originating widget
|
||||
this.$widget.find(`[data-ntx-id="${noteContext.ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${ntxId}"]`));
|
||||
}
|
||||
|
||||
|
||||
await appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
|
||||
|
||||
@ -5,14 +5,24 @@ body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
|
||||
|
||||
body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-dialog {
|
||||
border-bottom-left-radius: var(--bs-modal-border-radius);
|
||||
border-bottom-right-radius: var(--bs-modal-border-radius);
|
||||
}
|
||||
|
||||
body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
max-width: min(var(--preferred-max-content-width), 95vw);
|
||||
max-height: var(--tn-modal-max-height);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-content {
|
||||
transition: background-color 250ms ease-in;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-header .modal-title {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
@ -23,8 +33,8 @@ body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-header .title-row > * {
|
||||
margin: 5px;
|
||||
.modal.popup-editor-dialog .modal-header .note-title-widget {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-body {
|
||||
@ -52,13 +62,23 @@ body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .classic-toolbar-outer-container.visible {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog div.promoted-attributes-container {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .classic-toolbar-widget {
|
||||
position: sticky;
|
||||
margin-inline: 8px;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
background: var(--modal-background-color);
|
||||
z-index: 998;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail.full-height {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import "./PopupEditor.css";
|
||||
import { useNoteContext, useTriliumEvent } from "../react/hooks";
|
||||
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteContext from "../../components/note_context";
|
||||
@ -18,6 +18,9 @@ import utils from "../../services/utils";
|
||||
import tree from "../../services/tree";
|
||||
import froca from "../../services/froca";
|
||||
import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
|
||||
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
||||
import { t } from "../../services/i18n";
|
||||
import appContext from "../../components/app_context";
|
||||
|
||||
export default function PopupEditor() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
@ -59,6 +62,16 @@ export default function PopupEditor() {
|
||||
<DialogWrapper>
|
||||
<Modal
|
||||
title={<TitleRow />}
|
||||
customTitleBarButtons={[{
|
||||
iconClassName: "bx-expand-alt",
|
||||
title: t("popup-editor.maximize"),
|
||||
onClick: async () => {
|
||||
if (!noteContext.noteId) return;
|
||||
const { noteId, hoistedNoteId } = noteContext;
|
||||
await appContext.tabManager.openInNewTab(noteId, hoistedNoteId, true);
|
||||
setShown(false);
|
||||
}
|
||||
}]}
|
||||
className="popup-editor-dialog"
|
||||
size="lg"
|
||||
show={shown}
|
||||
@ -67,10 +80,15 @@ export default function PopupEditor() {
|
||||
}}
|
||||
onHidden={() => setShown(false)}
|
||||
keepInDom // needed for faster loading
|
||||
noFocus // automatic focus breaks block popup
|
||||
>
|
||||
<ReadOnlyNoteInfoBar />
|
||||
<PromotedAttributes />
|
||||
<StandaloneRibbonAdapter component={FormattingToolbar} />
|
||||
|
||||
{isMobile
|
||||
? <MobileEditorToolbar inPopupEditor />
|
||||
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
|
||||
|
||||
<FloatingButtons items={items} />
|
||||
<NoteDetail />
|
||||
<NoteList media="screen" displayOnlyCollections />
|
||||
@ -83,17 +101,10 @@ export default function PopupEditor() {
|
||||
export function DialogWrapper({ children }: { children: ComponentChildren }) {
|
||||
const { note } = useNoteContext();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [ hasTint, setHasTint ] = useState(false);
|
||||
|
||||
// Apply the tinted-dialog class only if the custom color CSS class specifies a hue
|
||||
useEffect(() => {
|
||||
if (!wrapperRef.current) return;
|
||||
const customHue = getComputedStyle(wrapperRef.current).getPropertyValue("--custom-color-hue");
|
||||
setHasTint(!!customHue);
|
||||
}, [ note ]);
|
||||
useNoteLabel(note, "color"); // to update color class
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""} ${hasTint ? "tinted-quick-edit-dialog" : ""}`}>
|
||||
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -31,29 +31,29 @@ export default function AboutDialog() {
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t("about.homepage")}</th>
|
||||
<td><a className="tn-link external" href="https://github.com/TriliumNext/Trilium" style={forceWordBreak}>https://github.com/TriliumNext/Trilium</a></td>
|
||||
<td className="selectable-text"><a className="tn-link external" href="https://github.com/TriliumNext/Trilium" style={forceWordBreak}>https://github.com/TriliumNext/Trilium</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.app_version")}</th>
|
||||
<td className="app-version">{appInfo?.appVersion}</td>
|
||||
<td className="app-version selectable-text">{appInfo?.appVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.db_version")}</th>
|
||||
<td className="db-version">{appInfo?.dbVersion}</td>
|
||||
<td className="db-version selectable-text">{appInfo?.dbVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.sync_version")}</th>
|
||||
<td className="sync-version">{appInfo?.syncVersion}</td>
|
||||
<td className="sync-version selectable-text">{appInfo?.syncVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.build_date")}</th>
|
||||
<td className="build-date">
|
||||
<td className="build-date selectable-text">
|
||||
{appInfo?.buildDate ? formatDateTime(appInfo.buildDate) : ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("about.build_revision")}</th>
|
||||
<td>
|
||||
<td className="selectable-text">
|
||||
{appInfo?.buildRevision && <a className="tn-link build-revision external" href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" style={forceWordBreak}>{appInfo.buildRevision}</a>}
|
||||
</td>
|
||||
</tr>
|
||||
@ -76,8 +76,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
|
||||
openService.openDirectory(directory);
|
||||
};
|
||||
|
||||
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a>
|
||||
return <a className="tn-link selectable-text" href="#" onClick={onClick} style={style}>{directory}</a>
|
||||
} else {
|
||||
return <span style={style}>{directory}</span>;
|
||||
return <span className="selectable-text" style={style}>{directory}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Modal from "../react/Modal.jsx";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { CommandNames } from "../../components/app_context.js";
|
||||
import appContext, { CommandNames } from "../../components/app_context.js";
|
||||
import RawHtml from "../react/RawHtml.jsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import keyboard_actions from "../../services/keyboard_actions.js";
|
||||
@ -14,6 +14,7 @@ export default function HelpDialog() {
|
||||
return (
|
||||
<Modal
|
||||
title={t("help.title")} className="help-dialog use-tn-links" minWidth="90%" size="lg" scrollable
|
||||
customTitleBarButtons={[{title: t("help.editShortcuts"), iconClassName: "bxs-pencil", onClick: editShortcuts}]}
|
||||
onHidden={() => setShown(false)}
|
||||
show={shown}
|
||||
>
|
||||
@ -160,3 +161,7 @@ function Card({ title, children }: { title: string, children: ComponentChildren
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function editShortcuts() {
|
||||
appContext.tabManager.openContextWithNote("_optionsShortcuts", { activate: true });
|
||||
}
|
||||
@ -34,15 +34,26 @@ export default function MarkdownImportDialog() {
|
||||
}
|
||||
});
|
||||
|
||||
function submit() {
|
||||
setShown(false);
|
||||
if (editorApiRef.current && text) {
|
||||
convertMarkdownToHtml(text, editorApiRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="markdown-import-dialog" title={t("markdown_import.dialog_title")} size="lg"
|
||||
footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={() => setShown(false)} keyboardShortcut="Ctrl+Enter" />}
|
||||
onShown={() => markdownImportTextArea.current?.focus()}
|
||||
onHidden={async () => {
|
||||
if (editorApiRef.current) {
|
||||
await convertMarkdownToHtml(text, editorApiRef.current);
|
||||
footer={
|
||||
<Button
|
||||
className="markdown-import-button"
|
||||
text={t("markdown_import.import_button")}
|
||||
keyboardShortcut="Ctrl+Enter"
|
||||
onClick={submit}
|
||||
/>
|
||||
}
|
||||
onShown={() => markdownImportTextArea.current?.focus()}
|
||||
onHidden={() => {
|
||||
setShown(false);
|
||||
setText("");
|
||||
}}
|
||||
@ -55,7 +66,7 @@ export default function MarkdownImportDialog() {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
setShown(false);
|
||||
submit();
|
||||
}
|
||||
}}></textarea>
|
||||
</Modal>
|
||||
|
||||
63
apps/client/src/widgets/dialogs/revisions.css
Normal file
63
apps/client/src/widgets/dialogs/revisions.css
Normal file
@ -0,0 +1,63 @@
|
||||
body.mobile .revisions-dialog {
|
||||
.modal-dialog {
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
height: fit-content !important;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.revision-list {
|
||||
height: fit-content !important;
|
||||
max-height: 20vh;
|
||||
border-bottom: 1px solid var(--main-border-color) !important;
|
||||
padding: 0 1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-body > .revision-content-wrapper {
|
||||
flex-grow: 1;
|
||||
max-width: unset !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.modal-body > .revision-content-wrapper > div:first-of-type {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.revision-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.revision-title-buttons {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.revision-content {
|
||||
padding: 0.5em;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@ import ActionButton from "../react/ActionButton";
|
||||
import options from "../../services/options";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { diffWords } from "diff";
|
||||
import "./revisions.css";
|
||||
|
||||
export default function RevisionsDialog() {
|
||||
const [ note, setNote ] = useState<FNote>();
|
||||
@ -137,7 +138,7 @@ export default function RevisionsDialog() {
|
||||
|
||||
function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) {
|
||||
return (
|
||||
<FormList onSelect={onSelect} fullHeight>
|
||||
<FormList onSelect={onSelect} fullHeight wrapperClassName="revision-list">
|
||||
{revisions.map((item) =>
|
||||
<FormListItem
|
||||
value={item.revisionId}
|
||||
@ -208,7 +209,7 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
|
||||
}
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="revision-content use-tn-links" style={{ overflow: "auto", wordBreak: "break-word" }}>
|
||||
<div className="revision-content use-tn-links selectable-text" style={{ overflow: "auto", wordBreak: "break-word" }}>
|
||||
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import appContext from "../../components/app_context";
|
||||
import contextMenu from "../../menus/context_menu";
|
||||
import appContext, { CommandMappings } from "../../components/app_context";
|
||||
import contextMenu, { MenuItem } from "../../menus/context_menu";
|
||||
import branches from "../../services/branches";
|
||||
import { t } from "../../services/i18n";
|
||||
import note_create from "../../services/note_create";
|
||||
import tree from "../../services/tree";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import BasicWidget from "../basic_widget";
|
||||
|
||||
export default function MobileDetailMenu() {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
@ -16,17 +17,33 @@ export default function MobileDetailMenu() {
|
||||
icon="bx bx-dots-vertical-rounded"
|
||||
text=""
|
||||
onClick={(e) => {
|
||||
const note = appContext.tabManager.getActiveContextNote();
|
||||
const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId();
|
||||
if (!ntxId) return;
|
||||
|
||||
contextMenu.show<"insertChildNote" | "delete" | "showRevisions">({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
|
||||
const subContexts = noteContext.getMainContext().getSubContexts();
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const note = noteContext.note;
|
||||
|
||||
const items: (MenuItem<keyof CommandMappings>)[] = [
|
||||
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
||||
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
||||
{ kind: "separator" },
|
||||
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" }
|
||||
],
|
||||
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" },
|
||||
{ kind: "separator" },
|
||||
subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" },
|
||||
!isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" }
|
||||
].filter(i => !!i) as MenuItem<keyof CommandMappings>[];
|
||||
|
||||
const lastItem = items.at(-1);
|
||||
if (lastItem && "kind" in lastItem && lastItem.kind === "separator") {
|
||||
items.pop();
|
||||
}
|
||||
|
||||
contextMenu.show<keyof CommandMappings>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
@ -46,7 +63,7 @@ export default function MobileDetailMenu() {
|
||||
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
|
||||
}
|
||||
} else if (command && parentComponent) {
|
||||
parentComponent.triggerCommand(command);
|
||||
parentComponent.triggerCommand(command, { ntxId });
|
||||
}
|
||||
},
|
||||
forcePositionOnMobile: true
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useNoteContext } from "../react/hooks";
|
||||
|
||||
export default function ToggleSidebarButton() {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const { noteContext, parentComponent } = useNoteContext();
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
<div style={{ contain: "none", minWidth: 8 }}>
|
||||
{ noteContext?.isMainContext() && <ActionButton
|
||||
icon="bx bx-sidebar"
|
||||
text={t("note_tree.toggle-sidebar")}
|
||||
onClick={() => parentComponent?.triggerCommand("setActiveScreen", {
|
||||
screen: "tree"
|
||||
})}
|
||||
/>
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -19,9 +19,11 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
|
||||
disabled?: boolean;
|
||||
text?: ComponentChildren;
|
||||
forceShown?: boolean;
|
||||
onShown?: () => void;
|
||||
onHidden?: () => void;
|
||||
}
|
||||
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown }: DropdownProps) {
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden }: DropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
@ -40,10 +42,12 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
|
||||
const onShown = useCallback(() => {
|
||||
setShown(true);
|
||||
externalOnShown?.();
|
||||
}, [])
|
||||
|
||||
const onHidden = useCallback(() => {
|
||||
setShown(false);
|
||||
externalOnHidden?.();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -6,15 +6,17 @@ import "./FormList.css";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
import { handleRightToLeftPlacement, isMobile } from "../../services/utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface FormListOpts {
|
||||
children: ComponentChildren;
|
||||
onSelect?: (value: string) => void;
|
||||
style?: CSSProperties;
|
||||
wrapperClassName?: string;
|
||||
fullHeight?: boolean;
|
||||
}
|
||||
|
||||
export default function FormList({ children, onSelect, style, fullHeight }: FormListOpts) {
|
||||
export default function FormList({ children, onSelect, style, fullHeight, wrapperClassName }: FormListOpts) {
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
@ -43,7 +45,7 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
|
||||
}, [ fullHeight ]);
|
||||
|
||||
return (
|
||||
<div className="dropdownWrapper" ref={wrapperRef} style={builtinStyles}>
|
||||
<div className={clsx("dropdownWrapper", wrapperClassName)} ref={wrapperRef} style={builtinStyles}>
|
||||
<div className="dropdown" style={builtinStyles}>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useMemo } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ComponentChildren } from "preact";
|
||||
@ -7,9 +8,16 @@ import { Modal as BootstrapModal } from "bootstrap";
|
||||
import { memo } from "preact/compat";
|
||||
import { useSyncedRef } from "./hooks";
|
||||
|
||||
interface CustomTitleBarButton {
|
||||
title: string;
|
||||
iconClassName: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ModalProps {
|
||||
className: string;
|
||||
title: string | ComponentChildren;
|
||||
customTitleBarButtons?: (CustomTitleBarButton | null)[];
|
||||
size: "xl" | "lg" | "md" | "sm";
|
||||
children: ComponentChildren;
|
||||
/**
|
||||
@ -66,9 +74,13 @@ interface ModalProps {
|
||||
* If true, the modal will remain in the DOM even when not shown. This can be useful for certain CSS transitions or when you want to avoid re-mounting the modal content.
|
||||
*/
|
||||
keepInDom?: boolean;
|
||||
/**
|
||||
* If true, the modal will not focus itself after becoming visible.
|
||||
*/
|
||||
noFocus?: boolean;
|
||||
}
|
||||
|
||||
export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom }: ModalProps) {
|
||||
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
||||
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||
const modalInstanceRef = useRef<BootstrapModal>();
|
||||
const elementToFocus = useRef<Element | null>();
|
||||
@ -100,13 +112,15 @@ export default function Modal({ children, className, size, title, header, footer
|
||||
useEffect(() => {
|
||||
if (show && modalRef.current) {
|
||||
elementToFocus.current = document.activeElement;
|
||||
openDialog($(modalRef.current), !stackable).then(($widget) => {
|
||||
openDialog($(modalRef.current), !stackable, {
|
||||
focus: !noFocus
|
||||
}).then(($widget) => {
|
||||
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
|
||||
})
|
||||
} else {
|
||||
modalInstanceRef.current?.hide();
|
||||
}
|
||||
}, [ show, modalRef.current ]);
|
||||
}, [ show, modalRef.current, noFocus ]);
|
||||
|
||||
// Memoize styles to prevent recreation on every render
|
||||
const dialogStyle = useMemo<CSSProperties>(() => {
|
||||
@ -142,7 +156,17 @@ export default function Modal({ children, className, size, title, header, footer
|
||||
{helpPageId && (
|
||||
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
|
||||
)}
|
||||
|
||||
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
|
||||
<button type="button"
|
||||
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
|
||||
title={titleBarButton.title}
|
||||
onClick={titleBarButton.onClick}>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
||||
|
||||
</div>
|
||||
|
||||
{onSubmit ? (
|
||||
|
||||
@ -461,26 +461,29 @@ export function useNoteLabelInt(note: FNote | undefined | null, labelName: Filte
|
||||
|
||||
export function useNoteBlob(note: FNote | null | undefined, componentId?: string): FBlob | null | undefined {
|
||||
const [ blob, setBlob ] = useState<FBlob | null>();
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
function refresh() {
|
||||
note?.getBlob().then(setBlob);
|
||||
async function refresh() {
|
||||
const requestId = ++requestIdRef.current;
|
||||
const newBlob = await note?.getBlob();
|
||||
|
||||
// Only update if this is the latest request.
|
||||
if (requestId === requestIdRef.current) {
|
||||
setBlob(newBlob);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note?.noteId ]);
|
||||
useEffect(() => { refresh() }, [ note?.noteId ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (!note) return;
|
||||
|
||||
// Check if the note was deleted.
|
||||
if (loadResults.getEntityRow("notes", note.noteId)?.isDeleted) {
|
||||
requestIdRef.current++; // invalidate pending results
|
||||
setBlob(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a revision occurred.
|
||||
if (loadResults.hasRevisionForNote(note.noteId)) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
if (loadResults.isNoteContentReloaded(note.noteId, componentId)) {
|
||||
refresh();
|
||||
}
|
||||
@ -806,7 +809,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
|
||||
setIsReadOnly(readOnly);
|
||||
});
|
||||
}
|
||||
}, [note, noteContext]);
|
||||
}, [ note, noteContext, noteContext?.viewScope ]);
|
||||
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
|
||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||
|
||||
@ -17,24 +17,24 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
|
||||
return (
|
||||
<div className="file-properties-widget">
|
||||
{note && (
|
||||
<table class="file-table">
|
||||
<table className="file-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="text-nowrap">{t("file_properties.note_id")}:</th>
|
||||
<td class="file-note-id">{note.noteId}</td>
|
||||
<th class="text-nowrap">{t("file_properties.original_file_name")}:</th>
|
||||
<td class="file-filename">{originalFileName ?? "?"}</td>
|
||||
<th className="text-nowrap">{t("file_properties.note_id")}:</th>
|
||||
<td className="file-note-id selectable-text">{note.noteId}</td>
|
||||
<th className="text-nowrap">{t("file_properties.original_file_name")}:</th>
|
||||
<td className="file-filename selectable-text">{originalFileName ?? "?"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-nowrap">{t("file_properties.file_type")}:</th>
|
||||
<td class="file-filetype">{note.mime}</td>
|
||||
<th class="text-nowrap">{t("file_properties.file_size")}:</th>
|
||||
<td class="file-filesize">{formatSize(blob?.contentLength ?? 0)}</td>
|
||||
<th className="text-nowrap">{t("file_properties.file_type")}:</th>
|
||||
<td className="file-filetype selectable-text">{note.mime}</td>
|
||||
<th className="text-nowrap">{t("file_properties.file_size")}:</th>
|
||||
<td className="file-filesize selectable-text">{formatSize(blob?.contentLength ?? 0)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colSpan={4}>
|
||||
<div class="file-buttons">
|
||||
<div className="file-buttons">
|
||||
<Button
|
||||
icon="bx bx-download"
|
||||
text={t("file_properties.download")}
|
||||
|
||||
@ -23,17 +23,17 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
|
||||
<div style={{ display: "flex", justifyContent: "space-evenly", margin: "10px" }}>
|
||||
<span>
|
||||
<strong>{t("image_properties.original_file_name")}:</strong>{" "}
|
||||
<span>{originalFileName ?? "?"}</span>
|
||||
<span className="selectable-text">{originalFileName ?? "?"}</span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<strong>{t("image_properties.file_type")}:</strong>{" "}
|
||||
<span>{note.mime}</span>
|
||||
<span className="selectable-text">{note.mime}</span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<strong>{t("image_properties.file_size")}:</strong>{" "}
|
||||
<span>{formatSize(blob?.contentLength)}</span>
|
||||
<span className="selectable-text">{formatSize(blob?.contentLength)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext
|
||||
|
||||
return (
|
||||
<div className="inherited-attributes-widget">
|
||||
<div className="inherited-attributes-container">
|
||||
<div className="inherited-attributes-container selectable-text">
|
||||
{inheritedAttributes?.length ? (
|
||||
joinElements(inheritedAttributes.map(attribute => (
|
||||
<InheritedAttribute
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
|
||||
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { t } from "../../services/i18n"
|
||||
@ -113,8 +113,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
function DevelopmentActions({ note }: { note: FNote }) {
|
||||
return (
|
||||
<>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem disabled>Development-only Actions</FormListItem>
|
||||
<FormListHeader text="Development-only Actions" />
|
||||
<FormListItem
|
||||
icon="bx bx-printer"
|
||||
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
|
||||
|
||||
@ -39,21 +39,21 @@ export default function NoteInfoTab({ note }: TabContext) {
|
||||
<>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.note_id")}:</span>
|
||||
<span className="note-info-id">{note.noteId}</span>
|
||||
<span className="note-info-id selectable-text">{note.noteId}</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.created")}:</span>
|
||||
<span>{formatDateTime(metadata?.dateCreated)}</span>
|
||||
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.modified")}:</span>
|
||||
<span>{formatDateTime(metadata?.dateModified)}</span>
|
||||
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.type")}:</span>
|
||||
<span>
|
||||
<span className="note-info-type">{note.type}</span>{' '}
|
||||
{note.mime && <span className="note-info-mime">({note.mime})</span>}
|
||||
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
@ -77,7 +77,7 @@ export default function NoteInfoTab({ note }: TabContext) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="note-sizes-wrapper">
|
||||
<span className="note-sizes-wrapper selectable-text">
|
||||
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
|
||||
{" "}
|
||||
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
|
||||
|
||||
@ -8,6 +8,7 @@ import NoteActions from "./NoteActions";
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||
import clsx from "clsx";
|
||||
|
||||
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
||||
|
||||
@ -63,9 +64,10 @@ export default function Ribbon() {
|
||||
}, [ computedTabs, activeTabIndex ]));
|
||||
|
||||
return (
|
||||
<div className="ribbon-container" style={{ contain: "none" }}>
|
||||
{noteContext?.viewScope?.viewMode === "default" && (
|
||||
<>
|
||||
<div
|
||||
className={clsx("ribbon-container", noteContext?.viewScope?.viewMode !== "default" && "hidden-ext")}
|
||||
style={{ contain: "none" }}
|
||||
>
|
||||
<div className="ribbon-top-row">
|
||||
<div className="ribbon-tab-container">
|
||||
{computedTabs && computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
|
||||
@ -117,8 +119,6 @@ export default function Ribbon() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
&& !(await noteContext?.isReadOnly()),
|
||||
toggleCommand: "toggleRibbonTabClassicEditor",
|
||||
content: FormattingToolbar,
|
||||
activate: true,
|
||||
activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"),
|
||||
stayInDom: true
|
||||
},
|
||||
{
|
||||
@ -50,7 +50,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
icon: "bx bx-calendar-edit",
|
||||
content: EditedNotesTab,
|
||||
show: ({ note }) => note?.hasOwnedLabel("dateNote"),
|
||||
activate: ({ note }) => (note?.getPromotedDefinitionAttributes().length === 0 || !options.is("promotedAttributesOpenInRibbon")) && options.is("editedNotesOpenInRibbon")
|
||||
activate: () => options.is("editedNotesOpenInRibbon")
|
||||
},
|
||||
{
|
||||
title: t("book_properties.book_properties"),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.note-detail-doc-content {
|
||||
padding: 15px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.note-detail-doc-content pre {
|
||||
|
||||
@ -1,3 +1,21 @@
|
||||
.note-detail-empty {
|
||||
container-type: size;
|
||||
padding-top: 50px;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.note-detail-empty > * {
|
||||
margin-inline: auto;
|
||||
max-width: 850px;
|
||||
padding-inline: 50px;
|
||||
}
|
||||
|
||||
@container (max-width: 600px) {
|
||||
.note-detail-empty > * {
|
||||
padding-inline: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-notes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -14,7 +32,8 @@
|
||||
|
||||
.workspace-notes .workspace-note:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--main-border-color);
|
||||
background-color: var(--icon-button-hover-background);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.note-detail-empty-results .aa-dropdown-menu {
|
||||
@ -24,6 +43,11 @@
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.empty-tab-search label {
|
||||
margin-bottom: 8px;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.empty-tab-search .note-autocomplete-input {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
@ -10,16 +10,16 @@ import FNote from "../../entities/fnote";
|
||||
import search from "../../services/search";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function Empty({ }: TypeWidgetProps) {
|
||||
export default function Empty({ ntxId }: TypeWidgetProps) {
|
||||
return (
|
||||
<>
|
||||
<WorkspaceSwitcher />
|
||||
<NoteSearch />
|
||||
<NoteSearch ntxId={ntxId ?? null} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteSearch() {
|
||||
function NoteSearch({ ntxId }: { ntxId: string | null }) {
|
||||
const resultsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const autocompleteRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@ -45,10 +45,9 @@ function NoteSearch() {
|
||||
if (!suggestion?.notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (activeContext) {
|
||||
activeContext.setNote(suggestion.notePath);
|
||||
const activeNoteContext = appContext.tabManager.getNoteContextById(ntxId) ?? appContext.tabManager.getActiveContext();
|
||||
if (activeNoteContext) {
|
||||
activeNoteContext.setNote(suggestion.notePath);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -28,6 +28,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
|
||||
de: null,
|
||||
en: "en",
|
||||
en_rtl: "en",
|
||||
"en-GB": "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
it: "it",
|
||||
|
||||
10
apps/client/src/widgets/type_widgets/NoteMap.css
Normal file
10
apps/client/src/widgets/type_widgets/NoteMap.css
Normal file
@ -0,0 +1,10 @@
|
||||
.note-detail-note-map {
|
||||
&>div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
import NoteMapEl from "../note_map/NoteMap";
|
||||
import { useRef } from "preact/hooks";
|
||||
import "./NoteMap.css";
|
||||
|
||||
export default function NoteMap({ note }: TypeWidgetProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -22,6 +22,7 @@ describe("Canvas i18n", () => {
|
||||
if (locale.contentOnly || locale.devOnly) continue;
|
||||
const languageCode = LANGUAGE_MAPPINGS[locale.id];
|
||||
if (!supportedLanguageCodes.has(languageCode)) {
|
||||
console.log("Supported locales:", Array.from(supportedLanguageCodes.values()).join(", "));
|
||||
expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Language } from "@excalidraw/excalidraw/i18n";
|
||||
import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
|
||||
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, string | null> = {
|
||||
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"] | null> = {
|
||||
ar: "ar-SA",
|
||||
cn: "zh-CN",
|
||||
de: "de-DE",
|
||||
en: "en",
|
||||
"en-GB": "en",
|
||||
en_rtl: "en",
|
||||
es: "es-ES",
|
||||
fr: "fr-FR",
|
||||
|
||||
@ -104,7 +104,7 @@ export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
|
||||
backups.map(({ mtime, filePath }) => (
|
||||
<tr>
|
||||
<td>{mtime ? formatDateTime(mtime) : "-"}</td>
|
||||
<td>{filePath}</td>
|
||||
<td className="selectable-text">{filePath}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
|
||||
@ -226,7 +226,7 @@ function CodeBlockPreview({ theme, wordWrap }: { theme: string, wordWrap: boolea
|
||||
|
||||
return (
|
||||
<div className="note-detail-readonly-text-content ck-content code-sample-wrapper">
|
||||
<pre className="hljs" style={{ marginBottom: 0 }}>
|
||||
<pre className="hljs selectable-text" style={{ marginBottom: 0 }}>
|
||||
<code className="code-sample" style={codeStyle} dangerouslySetInnerHTML={getHtml(code)} />
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@ -98,6 +98,14 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
editorApi: editorApiRef.current,
|
||||
});
|
||||
},
|
||||
insertDateTimeToTextCommand() {
|
||||
if (!editorApiRef.current) return;
|
||||
const date = new Date();
|
||||
const customDateTimeFormat = options.get("customDateTimeFormat");
|
||||
const dateString = utils.formatDateTime(date, customDateTimeFormat);
|
||||
|
||||
addTextToEditor(dateString);
|
||||
},
|
||||
// Include note functionality note
|
||||
addIncludeNoteToTextCommand() {
|
||||
if (!editorApiRef.current) return;
|
||||
@ -197,14 +205,6 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
});
|
||||
}
|
||||
|
||||
useTriliumEvent("insertDateTimeToText", ({ ntxId: eventNtxId }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
const date = new Date();
|
||||
const customDateTimeFormat = options.get("customDateTimeFormat");
|
||||
const dateString = utils.formatDateTime(date, customDateTimeFormat);
|
||||
|
||||
addTextToEditor(dateString);
|
||||
});
|
||||
useTriliumEvent("addTextToActiveEditor", ({ text }) => {
|
||||
if (!noteContext?.isActive()) return;
|
||||
addTextToEditor(text);
|
||||
|
||||
@ -41,7 +41,6 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
|
||||
|
||||
// React to included note changes.
|
||||
useTriliumEvent("refreshIncludedNote", ({ noteId }) => {
|
||||
console.log("Refresh ", noteId);
|
||||
if (!contentRef.current) return;
|
||||
refreshIncludedNote(contentRef.current, noteId);
|
||||
});
|
||||
@ -56,7 +55,7 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
|
||||
<>
|
||||
<RawHtmlBlock
|
||||
containerRef={contentRef}
|
||||
className={clsx("note-detail-readonly-text-content ck-content use-tn-links", codeBlockWordWrap && "word-wrap")}
|
||||
className={clsx("note-detail-readonly-text-content ck-content use-tn-links selectable-text", codeBlockWordWrap && "word-wrap")}
|
||||
tabindex={100}
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
html={blob?.content}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
body.mobile {
|
||||
.classic-toolbar-outer-container {
|
||||
contain: none !important;
|
||||
}
|
||||
@ -50,3 +51,4 @@
|
||||
.classic-toolbar-widget .ck.ck-button.ck-disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
@ -4,19 +4,23 @@ import "./mobile_editor_toolbar.css";
|
||||
import { isIOS } from "../../../services/utils";
|
||||
import { CKTextEditor, ClassicEditor } from "@triliumnext/ckeditor5";
|
||||
|
||||
interface MobileEditorToolbarProps {
|
||||
inPopupEditor?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism:
|
||||
*
|
||||
* - On iOS, because it does not respect the viewport meta value `interactive-widget=resizes-content`, we need to listen to window resizes and scroll and reposition the toolbar using absolute positioning.
|
||||
* - On Android, the viewport change makes the keyboard resize the content area, all we have to do is to hide the tab bar and global menu (handled in the global style).
|
||||
*/
|
||||
export default function MobileEditorToolbar() {
|
||||
export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolbarProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { note, noteContext, ntxId } = useNoteContext();
|
||||
const [ shouldDisplay, setShouldDisplay ] = useState(false);
|
||||
const [ dropdownActive, setDropdownActive ] = useState(false);
|
||||
|
||||
usePositioningOniOS(containerRef);
|
||||
usePositioningOniOS(!inPopupEditor, containerRef);
|
||||
|
||||
useEffect(() => {
|
||||
noteContext?.isReadOnly().then(isReadOnly => {
|
||||
@ -29,7 +33,10 @@ export default function MobileEditorToolbar() {
|
||||
if (eventNtxId !== ntxId || !containerRef.current) return;
|
||||
const toolbar = editor.ui.view.toolbar?.element;
|
||||
|
||||
if (!inPopupEditor) {
|
||||
repositionDropdowns(editor);
|
||||
}
|
||||
|
||||
if (toolbar) {
|
||||
containerRef.current.replaceChildren(toolbar);
|
||||
} else {
|
||||
@ -60,7 +67,7 @@ export default function MobileEditorToolbar() {
|
||||
)
|
||||
}
|
||||
|
||||
function usePositioningOniOS(wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||
function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||
const adjustPosition = useCallback(() => {
|
||||
if (!wrapperRef.current) return;
|
||||
let bottom = window.innerHeight - (window.visualViewport?.height || 0);
|
||||
@ -68,7 +75,7 @@ function usePositioningOniOS(wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIOS()) return;
|
||||
if (!isIOS() || !enabled) return;
|
||||
|
||||
window.visualViewport?.addEventListener("resize", adjustPosition);
|
||||
window.addEventListener("scroll", adjustPosition);
|
||||
@ -77,7 +84,7 @@ function usePositioningOniOS(wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||
window.visualViewport?.removeEventListener("resize", adjustPosition);
|
||||
window.removeEventListener("scroll", adjustPosition);
|
||||
};
|
||||
}, []);
|
||||
}, [ enabled ]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -55,6 +55,7 @@ export function buildClassicToolbar(multilineToolbar: boolean) {
|
||||
...TEXT_FORMATTING_GROUP,
|
||||
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
|
||||
},
|
||||
"formatPainter",
|
||||
"|",
|
||||
"fontColor",
|
||||
"fontBackgroundColor",
|
||||
@ -104,6 +105,7 @@ export function buildFloatingToolbar() {
|
||||
...TEXT_FORMATTING_GROUP,
|
||||
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
|
||||
},
|
||||
"formatPainter",
|
||||
"|",
|
||||
"fontColor",
|
||||
"fontBackgroundColor",
|
||||
|
||||
@ -19,3 +19,9 @@ export function onWheelHorizontalScroll(event: WheelEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
(event.currentTarget as HTMLElement).scrollLeft += event.deltaY + event.deltaX;
|
||||
}
|
||||
|
||||
export function getClosestNtxId(element: HTMLElement) {
|
||||
const closestNtxEl = element.closest<HTMLElement>("[data-ntx-id]");
|
||||
if (!closestNtxEl) return null;
|
||||
return closestNtxEl.dataset.ntxId ?? null;
|
||||
}
|
||||
|
||||
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