Merge pull request #202 from gamosoft/features/configurations

Features/configurations
This commit is contained in:
Guillermo Villar 2026-04-07 12:59:43 +02:00 committed by GitHub
commit b1df01eafe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 181 additions and 19 deletions

View File

@ -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(

View File

@ -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):

View File

@ -17,17 +17,17 @@
### 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
### 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
@ -59,6 +59,7 @@
- **Drag to link** - Drag notes or images into the editor to insert links
- **Click to navigate** - Jump between notes seamlessly
- **External links** - Open in new tabs automatically
- **URI protocols** - Supports `mailto:`, `tel:`, `ssh:`, `ftp:`, `slack:`, `discord:`, `teams:`, `zoom:`, `whatsapp:`, `telegram:`, `signal:`, `spotify:`, `steam:`, `magnet:`, and more
### Outline Panel
- **Table of Contents** - View all headings (H1-H6) in sidebar

View File

@ -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,9 @@ 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',
// Icon rail / panel state
activePanel: 'files', // 'files', 'search', 'tags', 'settings'
@ -852,6 +857,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 +1816,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 +1825,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
@ -1966,7 +2025,7 @@ 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);
@ -2521,7 +2580,9 @@ function noteApp() {
if (!href) return;
// Check if it's an external link or API path (media files, etc.)
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('/api/')) {
// Safe external protocols: http, https, mailto, tel, ssh, ftp, sftp, and app deep links
const externalProtocols = ['http://', 'https://', '//', 'mailto:', 'tel:', 'ssh:', 'ftp:', 'sftp:', 'slack:', 'discord:', 'teams:', 'vscode:', 'zoom:', 'whatsapp:', 'telegram:', 'signal:', 'spotify:', 'steam:', 'magnet:', '/api/'];
if (externalProtocols.some(p => href.startsWith(p))) {
return; // Let external links and API paths work normally
}

View File

@ -1591,6 +1591,14 @@
<div class="flex items-center justify-between mb-1">
<span x-text="t('sidebar.folders_and_notes')"></span>
<div class="flex gap-0.5">
<button
@click="cycleSortMode()"
class="text-xs px-1.5 py-0.5 rounded transition-all hover:scale-110"
:title="t('sidebar.sort_' + sortMode)"
style="opacity: 0.7;"
onmouseover="this.style.opacity='1'"
onmouseout="this.style.opacity='0.7'"
><span x-text="getSortIcon()"></span></button>
<button
@click="expandAllFolders()"
class="text-xs px-1.5 py-0.5 rounded transition-all hover:scale-110"
@ -1635,11 +1643,11 @@
</div>
<!-- Folders (rendered recursively for unlimited nesting) -->
<template x-for="[folderKey, folder] in Object.entries(folderTree).filter(([k, v]) => k !== '__root__' && (!hideUnderscoreFolders || !v.name.startsWith('_'))).sort((a, b) => a[1].name.toLowerCase().localeCompare(b[1].name.toLowerCase()))" :key="folderKey">
<template x-for="[folderKey, folder] in Object.entries(folderTree).filter(([k, v]) => k !== '__root__' && (!hideUnderscoreFolders || !v.name.startsWith('_'))).sort((a, b) => getFolderSortComparator()(a[1], b[1]))" :key="folderKey">
<div x-html="renderFolderRecursive(folder, 0, true)"></div>
</template>
<!-- Root notes and images (no folder) - rendered via JS for consistency -->
<!-- Root notes and images (no folder) -->
<div x-html="renderRootItems()"></div>
<!-- Empty state -->

View File

@ -49,6 +49,12 @@
"no_results": "Keine Ergebnisse gefunden",
"expand_all": "Alle Ordner aufklappen",
"collapse_all": "Alle Ordner zuklappen",
"sort_a-z": "Sortieren A-Z",
"sort_z-a": "Sortieren Z-A",
"sort_newest": "Nach neuesten sortieren",
"sort_oldest": "Nach ältesten sortieren",
"sort_largest": "Nach größten sortieren",
"sort_smallest": "Nach kleinsten sortieren",
"toggle_sidebar": "Seitenleiste umschalten",
"go_to_homepage": "Zur Startseite",
"files": "Dateien",

View File

@ -48,6 +48,12 @@
"no_results": "No results found",
"expand_all": "Expand all folders",
"collapse_all": "Collapse all folders",
"sort_a-z": "Sort A to Z",
"sort_z-a": "Sort Z to A",
"sort_newest": "Sort by newest",
"sort_oldest": "Sort by oldest",
"sort_largest": "Sort by largest",
"sort_smallest": "Sort by smallest",
"toggle_sidebar": "Toggle sidebar",
"go_to_homepage": "Go to homepage",
"files": "Files",

View File

@ -49,6 +49,12 @@
"no_results": "No results found",
"expand_all": "Expand all folders",
"collapse_all": "Collapse all folders",
"sort_a-z": "Sort A to Z",
"sort_z-a": "Sort Z to A",
"sort_newest": "Sort by newest",
"sort_oldest": "Sort by oldest",
"sort_largest": "Sort by largest",
"sort_smallest": "Sort by smallest",
"toggle_sidebar": "Toggle sidebar",
"go_to_homepage": "Go to homepage",
"files": "Files",

View File

@ -49,6 +49,12 @@
"no_results": "No se encontraron resultados",
"expand_all": "Expandir todas las carpetas",
"collapse_all": "Contraer todas las carpetas",
"sort_a-z": "Ordenar A-Z",
"sort_z-a": "Ordenar Z-A",
"sort_newest": "Ordenar por más reciente",
"sort_oldest": "Ordenar por más antiguo",
"sort_largest": "Ordenar por más grande",
"sort_smallest": "Ordenar por más pequeño",
"toggle_sidebar": "Alternar barra lateral",
"go_to_homepage": "Ir al inicio",
"files": "Archivos",

View File

@ -49,6 +49,12 @@
"no_results": "Aucun résultat trouvé",
"expand_all": "Développer tous les dossiers",
"collapse_all": "Réduire tous les dossiers",
"sort_a-z": "Trier A-Z",
"sort_z-a": "Trier Z-A",
"sort_newest": "Trier par plus récent",
"sort_oldest": "Trier par plus ancien",
"sort_largest": "Trier par plus grand",
"sort_smallest": "Trier par plus petit",
"toggle_sidebar": "Afficher/Masquer la barre latérale",
"go_to_homepage": "Aller à l'accueil",
"files": "Fichiers",

View File

@ -49,6 +49,12 @@
"no_results": "Nincs találat",
"expand_all": "Mappák kibontása",
"collapse_all": "Mappák összecsukása",
"sort_a-z": "Rendezés A-Z",
"sort_z-a": "Rendezés Z-A",
"sort_newest": "Legújabb elöl",
"sort_oldest": "Legrégebbi elöl",
"sort_largest": "Legnagyobb elöl",
"sort_smallest": "Legkisebb elöl",
"toggle_sidebar": "Oldalsáv megjelenítése",
"go_to_homepage": "Ugrás a kezdőlapra",
"files": "Fájlok",

View File

@ -48,6 +48,12 @@
"no_results": "Nessun risultato trovato",
"expand_all": "Espandi tutte le cartelle",
"collapse_all": "Comprimi tutte le cartelle",
"sort_a-z": "Ordina A-Z",
"sort_z-a": "Ordina Z-A",
"sort_newest": "Ordina per più recente",
"sort_oldest": "Ordina per più vecchio",
"sort_largest": "Ordina per più grande",
"sort_smallest": "Ordina per più piccolo",
"toggle_sidebar": "Mostra/nascondi barra laterale",
"go_to_homepage": "Vai alla home",
"files": "File",

View File

@ -48,6 +48,12 @@
"no_results": "結果が見つかりません",
"expand_all": "すべて展開",
"collapse_all": "すべて折りたたむ",
"sort_a-z": "A-Z順",
"sort_z-a": "Z-A順",
"sort_newest": "新しい順",
"sort_oldest": "古い順",
"sort_largest": "大きい順",
"sort_smallest": "小さい順",
"toggle_sidebar": "サイドバー切替",
"go_to_homepage": "ホームへ",
"files": "ファイル",

View File

@ -48,6 +48,12 @@
"no_results": "Ничего не найдено",
"expand_all": "Развернуть все папки",
"collapse_all": "Свернуть все папки",
"sort_a-z": "Сортировка А-Я",
"sort_z-a": "Сортировка Я-А",
"sort_newest": "Сначала новые",
"sort_oldest": "Сначала старые",
"sort_largest": "Сначала большие",
"sort_smallest": "Сначала маленькие",
"toggle_sidebar": "Показать/скрыть боковую панель",
"go_to_homepage": "На главную",
"files": "Файлы",

View File

@ -48,6 +48,12 @@
"no_results": "Ni najdenih rezultatov",
"expand_all": "Razširi vse mape",
"collapse_all": "Strni vse mape",
"sort_a-z": "Razvrsti A-Ž",
"sort_z-a": "Razvrsti Ž-A",
"sort_newest": "Razvrsti po najnovejših",
"sort_oldest": "Razvrsti po najstarejših",
"sort_largest": "Razvrsti po največjih",
"sort_smallest": "Razvrsti po najmanjših",
"toggle_sidebar": "Preklopi stransko vrstico",
"go_to_homepage": "Pojdi na začetno stran",
"files": "Datoteke",

View File

@ -48,6 +48,12 @@
"no_results": "未找到结果",
"expand_all": "展开所有文件夹",
"collapse_all": "折叠所有文件夹",
"sort_a-z": "按A-Z排序",
"sort_z-a": "按Z-A排序",
"sort_newest": "按最新排序",
"sort_oldest": "按最旧排序",
"sort_largest": "按最大排序",
"sort_smallest": "按最小排序",
"toggle_sidebar": "切换侧边栏",
"go_to_homepage": "前往主页",
"files": "文件",