From 7090e835b19b9ed2e09b838a608ca009cdee89d0 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Mon, 6 Apr 2026 15:23:30 +0200 Subject: [PATCH 1/4] configurable upload limits --- backend/main.py | 18 ++++++++++++------ documentation/ENVIRONMENT_VARIABLES.md | 20 ++++++++++++++++++++ documentation/FEATURES.md | 8 ++++---- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/backend/main.py b/backend/main.py index d4cb453..6499f8b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -224,6 +224,12 @@ app.add_middleware( DEMO_MODE = os.getenv('DEMO_MODE', 'false').lower() in ('true', '1', 'yes') ALREADY_DONATED = os.getenv('ALREADY_DONATED', 'false').lower() in ('true', '1', 'yes') +# Upload size limits (in MB) - configurable via environment variables +UPLOAD_MAX_IMAGE_MB = int(os.getenv('UPLOAD_MAX_IMAGE_MB', '10')) +UPLOAD_MAX_AUDIO_MB = int(os.getenv('UPLOAD_MAX_AUDIO_MB', '50')) +UPLOAD_MAX_VIDEO_MB = int(os.getenv('UPLOAD_MAX_VIDEO_MB', '100')) +UPLOAD_MAX_PDF_MB = int(os.getenv('UPLOAD_MAX_PDF_MB', '20')) + if DEMO_MODE: # Enable rate limiting for demo deployments limiter = Limiter(key_func=get_remote_address, default_limits=["200/hour"]) @@ -656,14 +662,14 @@ async def upload_media(request: Request, file: UploadFile = File(...), note_path # Validate file size - different limits for different types media_type = get_media_type(file.filename) if file.filename else None - # Size limits: images 10MB, audio 50MB, video 100MB, PDF 20MB + # Size limits (configurable via UPLOAD_MAX_*_MB environment variables) size_limits = { - 'image': 10 * 1024 * 1024, - 'audio': 50 * 1024 * 1024, - 'video': 100 * 1024 * 1024, - 'document': 20 * 1024 * 1024, + 'image': UPLOAD_MAX_IMAGE_MB * 1024 * 1024, + 'audio': UPLOAD_MAX_AUDIO_MB * 1024 * 1024, + 'video': UPLOAD_MAX_VIDEO_MB * 1024 * 1024, + 'document': UPLOAD_MAX_PDF_MB * 1024 * 1024, } - max_size = size_limits.get(media_type, 10 * 1024 * 1024) + max_size = size_limits.get(media_type, UPLOAD_MAX_IMAGE_MB * 1024 * 1024) if len(file_data) > max_size: raise HTTPException( diff --git a/documentation/ENVIRONMENT_VARIABLES.md b/documentation/ENVIRONMENT_VARIABLES.md index 45dae01..3dfe1b9 100644 --- a/documentation/ENVIRONMENT_VARIABLES.md +++ b/documentation/ENVIRONMENT_VARIABLES.md @@ -47,6 +47,26 @@ AUTHENTICATION_PASSWORD=mysecretpassword > > Haven't donated yet? [☕ Buy me a coffee](https://ko-fi.com/gamosoft) - it takes 30 seconds and makes my day! +### Upload Limits + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `UPLOAD_MAX_IMAGE_MB` | integer | `10` | Maximum image upload size in MB | +| `UPLOAD_MAX_AUDIO_MB` | integer | `50` | Maximum audio upload size in MB | +| `UPLOAD_MAX_VIDEO_MB` | integer | `100` | Maximum video upload size in MB | +| `UPLOAD_MAX_PDF_MB` | integer | `20` | Maximum PDF upload size in MB | + +#### Example: Allowing larger video uploads + +```bash +# Docker +docker run -e UPLOAD_MAX_VIDEO_MB=500 ... + +# Docker Compose +environment: + - UPLOAD_MAX_VIDEO_MB=500 +``` + ## 🎯 Configuration Priority Configuration is loaded in this order (later overrides earlier): diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md index 950be8a..dfb100c 100644 --- a/documentation/FEATURES.md +++ b/documentation/FEATURES.md @@ -17,10 +17,10 @@ ### Media Support - **Drag & drop upload** - Drop files from your file system directly into the editor - **Clipboard paste** - Paste images from clipboard with Ctrl+V -- **Images** - JPG, PNG, GIF, WebP (max 10MB) -- **Audio** - MP3, WAV, OGG, M4A (max 50MB) -- **Video** - MP4, WebM, MOV, AVI (max 100MB) -- **Documents** - PDF (max 20MB) +- **Images** - JPG, PNG, GIF, WebP (default max 10MB, configurable) +- **Audio** - MP3, WAV, OGG, M4A (default max 50MB, configurable) +- **Video** - MP4, WebM, MOV, AVI (default max 100MB, configurable) +- **Documents** - PDF (default max 20MB, configurable) - **In-app viewing** - View all media types directly in the sidebar - **Inline preview** - Audio/video players and PDF viewer embedded in notes From 1206e7e50fd097890b4cee5e34ba4aa61ce18d87 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Mon, 6 Apr 2026 16:00:00 +0200 Subject: [PATCH 2/4] added sorting option --- documentation/FEATURES.md | 2 +- frontend/app.js | 74 +++++++++++++++++++++++++++++++++++---- frontend/index.html | 14 ++++++-- locales/de-DE.json | 6 ++++ locales/en-GB.json | 6 ++++ locales/en-US.json | 6 ++++ locales/es-ES.json | 6 ++++ locales/fr-FR.json | 6 ++++ locales/hu-HU.json | 6 ++++ locales/it-IT.json | 6 ++++ locales/ja-JP.json | 6 ++++ locales/ru-RU.json | 6 ++++ locales/sl-SI.json | 6 ++++ locales/zh-CN.json | 6 ++++ 14 files changed, 146 insertions(+), 10 deletions(-) diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md index dfb100c..b8ef0db 100644 --- a/documentation/FEATURES.md +++ b/documentation/FEATURES.md @@ -27,7 +27,7 @@ ### Organization - **Folder hierarchy** - Organize notes in nested folders - **Drag & drop** - Move notes and folders effortlessly -- **Alphabetical sorting** - Find notes quickly +- **Flexible sorting** - Sort notes by name (A-Z, Z-A), date (newest, oldest), or size (largest, smallest) - **Rename anything** - Files and folders, instantly - **Visual tree view** - Expandable/collapsible navigation - **Hide system folders** - Toggle to hide `_attachments`, `_templates` and other underscore-prefixed folders from sidebar diff --git a/frontend/app.js b/frontend/app.js index 4acd1a0..ea09c09 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -21,6 +21,8 @@ const LOCAL_SETTINGS = { tagsExpanded: { key: 'tagsExpanded', type: 'boolean', default: false }, hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false }, tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false }, + // String settings + sortMode: { key: 'sortMode', type: 'string', default: 'a-z' }, // Number settings with validation sidebarWidth: { key: 'sidebarWidth', type: 'number', default: CONFIG.DEFAULT_SIDEBAR_WIDTH, min: 200, max: 600 }, editorWidth: { key: 'editorWidth', type: 'number', default: 50, min: 20, max: 80 }, @@ -219,6 +221,10 @@ function noteApp() { // Tab key inserts tab character instead of changing focus tabInsertsTab: localStorage.getItem('tabInsertsTab') === 'true', + // Note sorting mode (a-z, z-a, newest, oldest, largest, smallest) + sortMode: localStorage.getItem('sortMode') || 'a-z', + sortKey: 0, // Incremented to force tree re-render + // Icon rail / panel state activePanel: 'files', // 'files', 'search', 'tags', 'settings' @@ -852,6 +858,60 @@ function noteApp() { this.autoSave(); }, + // Sort mode configuration + sortModes: ['a-z', 'z-a', 'newest', 'oldest', 'largest', 'smallest'], + sortModeIcons: { + 'a-z': 'A↓', + 'z-a': 'Z↓', + 'newest': '🕐↓', + 'oldest': '🕐↑', + 'largest': '📄↓', + 'smallest': '📄↑' + }, + + // Cycle through sort modes + cycleSortMode() { + const currentIndex = this.sortModes.indexOf(this.sortMode); + const nextIndex = (currentIndex + 1) % this.sortModes.length; + this.sortMode = this.sortModes[nextIndex]; + localStorage.setItem('sortMode', this.sortMode); + // Rebuild tree to apply new sort order + this.buildFolderTree(); + }, + + // Get current sort icon + getSortIcon() { + return this.sortModeIcons[this.sortMode] || 'A↓'; + }, + + // Get sort comparator based on current mode (for notes) + getSortComparator() { + switch (this.sortMode) { + case 'z-a': + return (a, b) => b.name.toLowerCase().localeCompare(a.name.toLowerCase()); + case 'newest': + return (a, b) => (b.modified || '').localeCompare(a.modified || ''); + case 'oldest': + return (a, b) => (a.modified || '').localeCompare(b.modified || ''); + case 'largest': + return (a, b) => (b.size || 0) - (a.size || 0); + case 'smallest': + return (a, b) => (a.size || 0) - (b.size || 0); + case 'a-z': + default: + return (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + } + }, + + // Get sort comparator for folders (only A-Z/Z-A, others default to A-Z) + getFolderSortComparator() { + if (this.sortMode === 'z-a') { + return (a, b) => b.name.toLowerCase().localeCompare(a.name.toLowerCase()); + } + // Default: A-Z for all other modes + return (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }, + // Update syntax highlight overlay (debounced, called on input) updateSyntaxHighlight() { if (!this.syntaxHighlightEnabled) return; @@ -1757,7 +1817,7 @@ function noteApp() { const sortNotes = (obj) => { if (obj.notes && obj.notes.length > 0) { // Create a new sorted array instead of mutating for Alpine reactivity - obj.notes = [...obj.notes].sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); + obj.notes = [...obj.notes].sort(this.getSortComparator()); } if (obj.children && Object.keys(obj.children).length > 0) { Object.values(obj.children).forEach(child => sortNotes(child)); @@ -1766,7 +1826,7 @@ function noteApp() { // Sort notes in root (create new array for reactivity) if (tree['__root__'] && tree['__root__'].notes) { - tree['__root__'].notes = [...tree['__root__'].notes].sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); + tree['__root__'].notes = [...tree['__root__'].notes].sort(this.getSortComparator()); } // Sort notes in all folders @@ -1886,7 +1946,8 @@ function noteApp() { // Render folder recursively (helper for deep nesting) // Uses data-* attributes to store path/name, avoiding JS string escaping issues - renderFolderRecursive(folder, level = 0, isTopLevel = false) { + // sortKey parameter triggers Alpine re-render when sort mode changes + renderFolderRecursive(folder, level = 0, isTopLevel = false, sortKey = 0) { if (!folder) return ''; let html = ''; @@ -1966,8 +2027,8 @@ function noteApp() { if (folder.children && Object.keys(folder.children).length > 0) { const children = Object.entries(folder.children) .filter(([k, v]) => !this.hideUnderscoreFolders || !v.name.startsWith('_')) - .sort((a, b) => a[1].name.toLowerCase().localeCompare(b[1].name.toLowerCase())); - + .sort((a, b) => this.getFolderSortComparator()(a[1], b[1])); + children.forEach(([childKey, childFolder]) => { html += this.renderFolderRecursive(childFolder, 0, false); }); @@ -2033,7 +2094,8 @@ function noteApp() { }, // Render root-level items (notes and media not in any folder) - renderRootItems() { + // sortKey parameter triggers Alpine re-render when sort mode changes + renderRootItems(sortKey = 0) { const root = this.folderTree['__root__']; if (!root || !root.notes || root.notes.length === 0) { return ''; diff --git a/frontend/index.html b/frontend/index.html index a9ebe8d..1e7aa1f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1591,7 +1591,15 @@
- +