feat: icônes par type de fichier + filtre par type + fix preview PDF
1. Icônes de fichier contextuelles: - _fileIcon() JS et _file_icon() Python: mappe l'extension vers emoji - 📕 PDF, 🖼️ images, 🐍 Python, 📜 JS/TS, 🌐 HTML, 🎨 CSS, 📋 JSON, 🗃️ SQL, 💻 Shell, ⚡ PS1, 📝 MD, 📦 archives... - Appliqué dans sidebar (board.py) + page workspace (x-for + renderChildren) 2. Preview PDF: ne fetch plus le contenu binaire (caractères bizarres), affiche 📕 'PDF Document' dans le popup hover 3. Filtre par type: - Chips 📝 Pages | 📕 PDF | 🖼️ Img | 📜 Code | 📄 Text - _filterTree() accepte un paramètre fileType - Désélection par second clic sur le même chip
This commit is contained in:
+31
-6
@@ -140,6 +140,27 @@ def _build_page_tree(conn, parent_id: int | None, ws_key: str, depth: int = 0, m
|
||||
return items
|
||||
|
||||
|
||||
def _file_icon(name: str) -> str:
|
||||
"""Map file extension to emoji icon."""
|
||||
import re
|
||||
n = name.lower()
|
||||
if re.search(r'\.(png|jpe?g|gif|webp|svg|bmp|ico)$', n): return '🖼️'
|
||||
if n.endswith('.pdf'): return '📕'
|
||||
if re.search(r'\.(md|markdown)$', n): return '📝'
|
||||
if n.endswith('.py'): return '🐍'
|
||||
if re.search(r'\.(js|jsx|ts|tsx)$', n): return '📜'
|
||||
if re.search(r'\.(html?|xml)$', n): return '🌐'
|
||||
if n.endswith('.css'): return '🎨'
|
||||
if n.endswith('.json'): return '📋'
|
||||
if n.endswith('.sql'): return '🗃️'
|
||||
if re.search(r'\.(sh|bash|zsh)$', n): return '💻'
|
||||
if n.endswith('.ps1'): return '⚡'
|
||||
if re.search(r'\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$', n): return '📜'
|
||||
if re.search(r'\.(txt|log)$', n): return '📄'
|
||||
if re.search(r'\.(zip|tar|gz|rar|7z)$', n): return '📦'
|
||||
return '📄'
|
||||
|
||||
|
||||
def _load_workspace_pages(ws_cookie: str) -> list:
|
||||
"""Load top-level pages with children for the active workspace."""
|
||||
if not ws_cookie:
|
||||
@@ -153,11 +174,13 @@ def _load_workspace_pages(ws_cookie: str) -> list:
|
||||
).fetchall()
|
||||
items = []
|
||||
for r in rows:
|
||||
is_folder = r["parent_section"] == "Workspace"
|
||||
title = r["title"] or "Untitled"
|
||||
items.append({
|
||||
"db_id": r["id"], "name": r["title"] or "Untitled",
|
||||
"db_id": r["id"], "name": title,
|
||||
"id": f"page/{r['id']}",
|
||||
"icon": "📁" if r["parent_section"] == "Workspace" else "📄",
|
||||
"is_folder": r["parent_section"] == "Workspace",
|
||||
"icon": "📁" if is_folder else _file_icon(title),
|
||||
"is_folder": is_folder,
|
||||
"children": _load_children(r["id"]),
|
||||
})
|
||||
return items
|
||||
@@ -174,11 +197,13 @@ def _load_children(parent_id: int) -> list:
|
||||
).fetchall()
|
||||
children = []
|
||||
for r in rows:
|
||||
is_folder = r["parent_section"] == "Workspace"
|
||||
title = r["title"] or "Untitled"
|
||||
children.append({
|
||||
"db_id": r["id"], "name": r["title"] or "Untitled",
|
||||
"db_id": r["id"], "name": title,
|
||||
"id": f"page/{r['id']}",
|
||||
"icon": "📁" if r["parent_section"] == "Workspace" else "📄",
|
||||
"is_folder": r["parent_section"] == "Workspace",
|
||||
"icon": "📁" if is_folder else _file_icon(title),
|
||||
"is_folder": is_folder,
|
||||
"children": _load_children(r["id"]),
|
||||
})
|
||||
return children
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
.search-clear{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:16px;padding:2px 4px;border-radius:3px;}
|
||||
.search-clear:hover{color:var(--text);background:var(--bg-hover);}
|
||||
|
||||
/* Filter chips */
|
||||
.filter-chips{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;}
|
||||
.filter-chip{padding:4px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--text-secondary);cursor:pointer;transition:all 150ms;}
|
||||
.filter-chip:hover{background:var(--bg-hover);color:var(--text);}
|
||||
.filter-chip.active{background:var(--accent);color:#fff;border-color:var(--accent);}
|
||||
|
||||
/* File preview tooltip */
|
||||
.file-preview{position:fixed;z-index:300;background:#1a1a1a;background:var(--bg,#1a1a1a);border:1px solid var(--border-strong,#333);border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.5);max-width:400px;min-width:200px;pointer-events:auto;}
|
||||
.file-preview-header{padding:8px 12px;font-size:12px;font-weight:600;color:var(--text-secondary,#888);border-bottom:1px solid var(--border,#333);}
|
||||
@@ -174,6 +180,15 @@
|
||||
<button class="search-clear" x-show="searchQuery" @click="searchQuery='';doFilter()" title="Clear">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter chips -->
|
||||
<div class="filter-chips" x-show="tree.length>0">
|
||||
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="FlowDeck pages">📝 Pages</button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF">📕 PDF</button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images">🖼️ Img</button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code">📜 Code</button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text">📄 Text</button>
|
||||
</div>
|
||||
|
||||
<ul class="ws-tree" x-show="displayTree.length>0" @click="handleTreeAction($event)">
|
||||
<template x-for="node in displayTree" :key="node.id">
|
||||
<li>
|
||||
@@ -192,7 +207,7 @@
|
||||
<span style="width:16px;flex-shrink:0;"></span>
|
||||
</template>
|
||||
|
||||
<span class="icon" x-text="node.is_folder ? '📁' : '📄'"></span>
|
||||
<span class="icon" x-text="_fileIcon(node.name, node.is_folder)"></span>
|
||||
|
||||
<template x-if="node.is_folder">
|
||||
<span class="name folder" @click.stop="navigateToFolder(node.id)" x-text="node.name"></span>
|
||||
@@ -306,6 +321,7 @@ document.addEventListener('alpine:init', function() {
|
||||
})(),
|
||||
currentFolder: {{ current_folder_id }},
|
||||
searchQuery: '',
|
||||
filterType: '',
|
||||
|
||||
// Preview popup
|
||||
previewVisible: false,
|
||||
@@ -360,7 +376,7 @@ document.addEventListener('alpine:init', function() {
|
||||
for (var i=0; i<children.length; i++) {
|
||||
var c = children[i];
|
||||
var hasKids = c.children && c.children.length > 0;
|
||||
var icon = c.is_folder ? '📁' : '📄';
|
||||
var icon = this._fileIcon(c.name, c.is_folder);
|
||||
var escName = this.esc(c.name);
|
||||
html += '<li><div class="ws-tree-item" style="padding-left:'+(depth*20)+'px" draggable="true"'
|
||||
+ ' data-folder-id="'+(c.is_folder?c.id:'')+'"'
|
||||
@@ -499,19 +515,58 @@ document.addEventListener('alpine:init', function() {
|
||||
}
|
||||
},
|
||||
|
||||
// ── File type icon ──
|
||||
_fileIcon(name, isFolder) {
|
||||
if (isFolder) return '📁';
|
||||
var n = (name || '').toLowerCase();
|
||||
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '🖼️';
|
||||
if (/\.pdf$/.test(n)) return '📕';
|
||||
if (/\.(md|markdown)$/.test(n)) return '📝';
|
||||
if (/\.(py)$/.test(n)) return '🐍';
|
||||
if (/\.(js|jsx|ts|tsx)$/.test(n)) return '📜';
|
||||
if (/\.(html?|xml)$/.test(n)) return '🌐';
|
||||
if (/\.css$/.test(n)) return '🎨';
|
||||
if (/\.json$/.test(n)) return '📋';
|
||||
if (/\.sql$/.test(n)) return '🗃️';
|
||||
if (/\.(sh|bash|zsh)$/.test(n)) return '💻';
|
||||
if (/\.ps1$/.test(n)) return '⚡';
|
||||
if (/\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$/.test(n)) return '📜';
|
||||
if (/\.(txt|log)$/.test(n)) return '📄';
|
||||
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return '📦';
|
||||
return '📄';
|
||||
},
|
||||
|
||||
// ── Search / Filter ──
|
||||
doFilter() {
|
||||
var q = (this.searchQuery || '').toLowerCase().trim();
|
||||
if (!q) { this.displayTree = this.tree; return; }
|
||||
this.displayTree = this._filterTree(this.tree, q);
|
||||
var ft = this.filterType || '';
|
||||
if (!q && !ft) { this.displayTree = this.tree; return; }
|
||||
var self = this;
|
||||
this.displayTree = this._filterTree(this.tree, q, ft);
|
||||
},
|
||||
_filterTree(nodes, query) {
|
||||
filterByType(type) {
|
||||
this.filterType = (this.filterType === type) ? '' : type;
|
||||
this.doFilter();
|
||||
},
|
||||
_filterTree(nodes, query, fileType) {
|
||||
var result = [];
|
||||
for (var i=0; i<nodes.length; i++) {
|
||||
var n = nodes[i];
|
||||
var filteredChildren = n.children ? this._filterTree(n.children, query) : [];
|
||||
var selfMatches = (n.name || '').toLowerCase().indexOf(query) !== -1;
|
||||
if (selfMatches || filteredChildren.length > 0) {
|
||||
var filteredChildren = n.children ? this._filterTree(n.children, query, fileType) : [];
|
||||
var selfMatches = (n.name || '').toLowerCase().indexOf(query || '') !== -1;
|
||||
// File type filter
|
||||
var typeMatch = true;
|
||||
if (fileType && !n.is_folder) {
|
||||
var nm = (n.name || '').toLowerCase();
|
||||
switch(fileType) {
|
||||
case 'pdf': typeMatch = /\.pdf$/.test(nm); break;
|
||||
case 'image': typeMatch = /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(nm); break;
|
||||
case 'code': typeMatch = /\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(nm); break;
|
||||
case 'text': typeMatch = /\.(txt|md|markdown|log)$/.test(nm); break;
|
||||
case 'page': typeMatch = !/\.(pdf|png|jpe?g|gif|webp|svg|bmp|ico|py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql|txt|md|markdown|log)$/.test(nm); break;
|
||||
}
|
||||
}
|
||||
if ((selfMatches || filteredChildren.length > 0) && typeMatch) {
|
||||
var clone = Object.assign({}, n);
|
||||
clone.children = filteredChildren;
|
||||
result.push(clone);
|
||||
@@ -542,6 +597,8 @@ document.addEventListener('alpine:init', function() {
|
||||
var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
|
||||
if (isImg) {
|
||||
this.previewContent = '<img src="'+url+'" style="max-width:380px;max-height:180px;display:block;">';
|
||||
} else if ((node.name||'').toLowerCase().endsWith('.pdf')) {
|
||||
this.previewContent = '<div style="text-align:center;padding:16px;"><span style="font-size:48px;">📕</span><br><span style="color:var(--text-secondary);font-size:12px;">PDF Document</span></div>';
|
||||
} else {
|
||||
this.previewContent = 'Loading...';
|
||||
fetch(url)
|
||||
|
||||
Reference in New Issue
Block a user