Merge pull request #202 from gamosoft/features/configurations
Features/configurations
This commit is contained in:
commit
b1df01eafe
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,8 +2025,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);
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1591,7 +1591,15 @@
|
|||
<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
|
||||
<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"
|
||||
:title="t('sidebar.expand_all')"
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "ファイル",
|
||||
|
|
|
|||
|
|
@ -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": "Файлы",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "文件",
|
||||
|
|
|
|||
Loading…
Reference in New Issue