feat: iconenen aangepast

This commit is contained in:
kodi
2026-03-12 15:35:47 +01:00
parent fc22550e91
commit aac84a0a7f
6 changed files with 565 additions and 50 deletions
+196
View File
@@ -0,0 +1,196 @@
# File Type Icons v1
## 1. Doel
Bestandstype-iconen voegen waarde toe omdat de mediaslot links van de naam dan niet alleen een lege placeholder of generiek bestandssymbool is, maar een snelle visuele hint geeft over het soort bestand. Dat maakt de lijst scanbaarder zonder de dual-pane file manager om te vormen tot een media- of documentbrowser.
Dit past goed naast de bestaande image thumbnails:
- afbeeldingen kunnen visueel herkend worden via echte thumbnails
- overige bestandstypen krijgen een compact, voorspelbaar icoon
- de naamkolom blijft strak uitgelijnd
De winst zit vooral in:
- sneller herkennen van directories, videos, pdfs en codebestanden
- rustigere lijst dan met overal dezelfde generieke file-icon
- geen extra backendcomplexiteit
## 2. Scope
Aanbevolen v1-scope voor eigen icon-types:
- directory
- generic file
- image
- video
- pdf
- text
- markdown
- json
- yaml/yml
- css
- javascript
- typescript
- html
- xml
- shell
- python
- dockerfile/containerfile
Niet in v1:
- merk- of app-specifieke iconensets
- tientallen niche-extensies
- dynamische OS-native icon lookup
- thumbnails voor video/pdf/documenten
## 3. Mediaslot gedrag
Aanbevolen gedrag in de bestaande mediaslot links van de naam:
- image + thumbnails aan:
- echte thumbnail
- image + thumbnails uit:
- image-icoon of generiek file-icoon
- directory:
- folder-icoon
- video:
- video-icoon
- pdf:
- pdf-icoon
- text / markdown / config / codebestanden:
- passend type-icoon
- onbekend type:
- generic file-icoon
Belangrijke regel:
- de mediaslot blijft altijd bestaan
- thumbnails en iconen delen exact dezelfde vaste slotbreedte
- de rij-uitlijning verandert dus niet per bestandstype of setting
## 4. Visuele stijl
Aanbevolen stijlrichting:
- compacte, rustige iconen
- eenvoudige vormen
- subtiele kleurcodering per categorie
- geen zware gradients
- geen drukke illustraties
- geen letterlijke macOS-kopie, maar wel dezelfde nette soberheid
Praktische ontwerprichtlijnen:
- folder-icoon iets warmer/neutraal
- image-icoon subtiel groen/blauw
- video-icoon subtiel paars/blauwgrijs of neutraal media-accent
- pdf-icoon ingetogen roodaccent
- code/configtypes vooral via kleine accentkleur en eenvoudig documentvorm-icoon
- generic file-icoon neutraal grijs/blauwgrijs
Doel:
- herkenbaarheid zonder visuele herrie
- lijst moet primair een file manager lijst blijven, niet een dashboard met bonte badges
## 5. Technische richting
Aanbevolen v1-richting:
- frontend-only
- geen backendwijzigingen
- geen nieuwe dependencies
Beste implementatievorm voor v1:
- inline SVG of kleine SVG-templates in frontendcode
- gekoppeld aan CSS classes voor kleur en grootte
Waarom niet via externe icon library:
- extra dependency zonder echte noodzaak
- grotere bundle en meer onderhoud
- minder controle over rustige, consistente stijl
Waarom niet puur CSS-only iconen:
- mogelijk, maar minder flexibel en vaak minder helder voor meerdere bestandscategorieën
Waarom inline SVG waarschijnlijk het best is:
- lichtgewicht
- goed stijlbaar met CSS
- makkelijk compact te houden
- eenvoudig uit te breiden met extra typen later
## 6. Extensie/content-type mapping
Aanbevolen mappingstrategie:
- primair op bestandsnaam/extensie
- speciale bestandsnamen expliciet ondersteunen
Voorgestelde regels:
- directory -> `folder`
- `jpg`, `jpeg`, `png`, `webp`, `gif`, `bmp`, `avif` -> `image`
- `mp4`, `mkv`, `mov`, `avi`, `webm` -> `video`
- `pdf` -> `pdf`
- `txt`, `log`, `ini`, `cfg`, `conf` -> `text`
- `md`, `markdown` -> `markdown`
- `json` -> `json`
- `yaml`, `yml` -> `yaml`
- `css` -> `css`
- `js`, `mjs`, `cjs` -> `javascript`
- `ts`, `tsx` -> `typescript`
- `html`, `htm` -> `html`
- `xml` -> `xml`
- `sh`, `bash`, `zsh`, `fish` -> `shell`
- `py` -> `python`
- `Dockerfile` / `Containerfile` / gelijknamige extensieloze bestandsnamen -> `docker`
- fallback -> `file`
Belangrijk:
- content-type detectie vanuit backend is hier niet nodig voor v1
- frontendmapping is voldoende en onderhoudbaar
- thumbnails voor images blijven de echte afbeelding gebruiken als de setting aan staat
## 7. Regressierisico
Belangrijkste risicos:
- mediaslot-uitlijning verschuift
- iconen worden te dominant of te kleurrijk
- thumbnails en iconen krijgen verschillende afmetingen
- current row / selected row styling verliest contrast
- de lijst wordt visueel drukker in plaats van rustiger
Beheersmaatregelen:
- vaste mediaslotbreedte behouden
- identieke bounding box voor iconen en thumbnails
- iconen klein en ingetogen houden
- selectie/current row styling boven iconstyling laten domineren
- geen extra badges, labels of tekstchips in de rij toevoegen
## 8. Teststrategie
### UI smoke/regressietests
- mediaslot blijft aanwezig
- directory render gebruikt folder-icoon-class
- pdf/video/image/generic typeclasses zijn aanwezig in renderlogica
- thumbnails aan/uit blijft de mediaslot intact houden
- lijst render met current row en selected row blijft bestaan
### Handmatige validatie
- directorys zijn snel herkenbaar
- image zonder thumbnail toont netjes image-icoon
- image met thumbnail toont echte thumbnail zonder layoutverschuiving
- video toont video-icoon
- pdf toont pdf-icoon
- code/configbestanden voelen visueel onderscheidbaar maar niet schreeuwerig
- inactive pane blijft rustig leesbaar
- naamkolom blijft voldoende breed en scanbaar
## 9. Aanbeveling
Aanbevolen v1-richting met laag regressierisico:
- frontend-only
- inline SVG-iconen of kleine herbruikbare SVG-templates
- mapping op extensie/bestandsnaam
- vaste mediaslot behouden
- thumbnails alleen voor images als `Show thumbnails` aan staat
- voor alle overige typen een rustig, compact type-icoon
- onbekende types terug laten vallen op generic file-icoon
Kort samengevat:
- geen ffmpeg
- geen video thumbnails
- geen backendwijzigingen
- geen icon library dependency
- wel een kleine, onderhoudbare uitbreiding van de bestaande mediaslot zodat de lijst visueel slimmer en rustiger wordt
+231
View File
@@ -0,0 +1,231 @@
# Video Thumbnail v1
## 1. Doel
Video thumbnails voegen vooral waarde toe in directories met veel mediabestanden: de gebruiker kan sneller onderscheid maken tussen vergelijkbare videobestanden zonder elk bestand eerst te openen. Binnen de huidige dual-pane workflow moet dit dezelfde ondersteunende rol hebben als image thumbnails: een kleine visuele hint in de bestaande mediaslot links van de naam, niet een aparte mediabrowser.
Dit moet passen binnen de huidige lijstweergave:
- geen galerij-UI
- geen wijziging aan browse- of selectiegedrag
- alleen zichtbaar als `Show thumbnails` is ingeschakeld
## 2. Scope
Aanbevolen v1-scope:
- wel onderzoeken en eventueel ondersteunen:
- `mp4`
- beperkt / best-effort:
- `mkv`
- niet in v1:
- video thumbnail-generatie zonder duidelijke technische basis
- video contact sheets
- hover-preview / animated preview
- pdf thumbnails
- subtitle-preview
- generieke media analysis pipeline
Belangrijke conclusie voor scope:
- `mp4` is de realistische primaire kandidaat
- `mkv` is niet gelijkwaardig aan `mp4`
- als `mkv` wordt meegenomen, moet dat expliciet best-effort zijn en niet als gegarandeerde happy path worden gepresenteerd
## 3. Technische haalbaarheid
### Zonder extra dependency
Echte video thumbnails genereren zonder extra dependency is in de praktijk niet verstandig.
Waarom:
- een browser kan wel video afspelen, maar de backend heeft voor een lijstthumbnail een afbeeldingsrepresentatie nodig
- pure standaardbibliotheek in Python biedt geen bruikbare frame-extractie uit video
- alleen het originele videobestand serveren en hopen dat de browser daar in een `<img>` of simpele lijstweergave iets van maakt is geen robuuste aanpak
Conclusie zonder dependency:
- echte video thumbnails zijn niet netjes haalbaar
- zonder extra tool resteert alleen een video-icoon/placeholder
### Met dependency zoals `ffmpeg`
`ffmpeg` is de technisch logische kandidaat voor frame-extractie.
Voordelen:
- breed inzetbaar
- kan één frame op een gekozen offset extraheren
- werkt voor veel containers/codecs, inclusief veel `mp4`- en `mkv`-varianten
- laat read-only thumbnailgeneratie toe zonder transcoding of volledige media pipeline
Nadelen:
- extra runtime dependency
- operationele afhankelijkheid op container/host
- frame-extractie is CPU- en I/O-werk
- caching- en invalidatievragen worden relevant
- `mkv` blijft afhankelijk van werkelijke codec-ondersteuning in de file, ook al helpt `ffmpeg` veel
### Aanbeveling haalbaarheid
Eerlijke v1-aanbeveling:
- **zonder extra dependency geen echte video thumbnails bouwen**
- als video thumbnails echt gewenst zijn, dan is een beperkte `ffmpeg`-afhankelijke v1 de eerste technische route die verdedigbaar is
- als het project op dit moment dependency-arm moet blijven, is uitstel verstandiger dan een halfwerkende pseudo-thumbnailoplossing
## 4. Thumbnail-bron
Als video thumbnails later wél gebouwd worden, is het eerste frame meestal geen goede keuze:
- eerste frames zijn vaak zwart
- leader-frames geven weinig informatie
Veiligere keuze:
- een klein offsetmoment, bijvoorbeeld rond `00:00:02` of een klein percentage in het begin
- nog steeds read-only: alleen één frame extraheren, geen analysepipeline
Bij grote bestanden en netwerkvolumes betekent dit:
- thumbnailgeneratie veroorzaakt extra read-I/O op het videobestand
- bij veel bestanden in één directory kan dat snel oplopen
- juist daarom is caching of strikte lazy loading relevant zodra echte video thumbnails worden toegevoegd
## 5. UI-gedrag
De video thumbnail moet in dezelfde mediaslot komen als image thumbnails:
- links van de naam
- zelfde vaste slotbreedte
- geen verschuiving van de naamkolom
Gedrag:
- als `Show thumbnails` uit staat:
- video krijgt gewoon het bestaande file-icoon of een video-icoon
- als `Show thumbnails` aan staat:
- ondersteunde video met beschikbare thumbnail: toon thumbnail
- als thumbnail niet beschikbaar is of niet ondersteund wordt: toon passend icoon/placeholder
Belangrijke UI-eis:
- de lijst mag niet instabiel worden
- thumbnails zijn een enhancement, geen vereiste voor nette uitlijning
## 6. Settings-relatie
Video thumbnails moeten alleen zichtbaar zijn als `Show thumbnails` aan staat.
Aanbevolen v1-keuze:
- **geen aparte extra setting voor video thumbnails**
Reden:
- een tweede thumbnailsetting maakt Settings complexer voordat er een bewezen implementatie is
- eerst moet duidelijk zijn of video thumbnails technisch en performance-matig verantwoord zijn
Als video thumbnails later worden toegevoegd, vallen ze in eerste instantie onder dezelfde globale toggle.
## 7. Performance en caching
Belangrijkste risico:
- frame-extractie is duidelijk duurder dan het serveren van bestaande image files
### Risico's
- grote directories met veel videos kunnen browse-performance merkbaar verslechteren
- thumbnails op netwerkvolumes versterken latency
- gelijktijdige extracties kunnen CPU en disk-I/O onnodig belasten
### Lazy loading
Lazy loading is verplicht zodra echte video thumbnails bestaan:
- alleen thumbnails laden voor zichtbare rijen
- beperkte concurrency
- geen eager generatie voor volledige directorylisting
### Caching
Voor echte video thumbnails is een vorm van caching vrijwel onvermijdelijk zodra de feature meer dan experimenteel moet zijn.
Afweging:
- zonder cache: te veel herhaalde frame-extractie
- met disk-cache: meer complexiteit, maar technisch logisch
Aanbevolen v1-richting als `ffmpeg` later wordt toegestaan:
- kleine disk-cache of bestandsgebonden cache-key op pad + mtime
- maar alleen als de implementatieslice expliciet bereid is deze extra complexiteit te dragen
Aanbevolen richting voor nu met laag risico:
- geen caching bouwen zolang de dependency- en implementatiekeuze nog niet definitief is
- eerst expliciet besluiten of echte video thumbnails de extra complexiteit waard zijn
## 8. Backend-impact
Als video thumbnails later worden gebouwd, is een apart read-only endpoint logisch, bijvoorbeeld:
- `GET /api/files/video-thumbnail?path=...`
Waarom apart endpoint:
- scheidt image thumbnails van videothumbnails
- maakt eigen foutafhandeling, typevalidatie en caching later eenvoudiger
- houdt browse-response klein
Bestaande infrastructuur moet leidend blijven:
- `path_guard`
- whitelist/root containment
- bestaande foutmapping voor traversal / invalid root alias / not found / type conflicts waar passend
- read-only gedrag: alleen lezen, geen wijziging aan bronbestand
## 9. MKV-realiteit
`mkv` is een container, geen garantie voor uniforme technische behandeling.
Belangrijke realiteit:
- `mkv`-bestanden variëren sterk in codecs en encoding-eigenschappen
- browser playback en server-side frame-extractie zijn twee verschillende dingen
- zelfs als browserplayback soms lastig is, kan `ffmpeg` vaak nog wel een thumbnail extraheren
- maar dat maakt `mkv` nog niet gelijkwaardig qua voorspelbaarheid aan `mp4`
Aanbevolen v1-positionering:
- `mkv` hooguit best-effort
- `mp4` is de primaire en verwachte happy path
- als thumbnail-extractie voor `mkv` faalt, toon gewoon het video-icoon/placeholder zonder extra dramatische fout in de lijst
## 10. Regressierisico
Belangrijkste regressierisico's:
- browse-performance verslechtert merkbaar
- thumbnail-latency maakt de lijst onrustig
- mediaslot wordt inconsistent tussen image/video/non-image
- externe toolchain verhoogt deploy-complexiteit
- caching kan nieuwe foutbronnen introduceren
Ook belangrijk:
- huidige image thumbnail-flow moet niet vervuild raken met video-specifieke uitzonderingen
- de lijst moet leesbaar blijven, ook als video thumbnails traag of afwezig zijn
## 11. Teststrategie
### Backend golden tests
Als video thumbnails later echt gebouwd worden:
- success voor ondersteunde video met thumbnail
- not found
- traversal blocked
- invalid root alias
- unsupported/non-video type blocked
- nette fallback als thumbnail-extractie niet lukt
### UI smoke/regressietests
- mediaslot blijft bestaan
- lijst blijft renderen met thumbnails aan/uit
- video zonder thumbnail toont icoon/placeholder
- image thumbnails blijven werken
- selectie/current row/active pane styling blijft intact
### Handmatige validatie
- directory met veel videos blijft bruikbaar
- thumbnails verschijnen alleen als setting aan staat
- mp4 thumbnail werkt voorspelbaar
- mkv faalt netjes terug op placeholder waar nodig
- geen merkbare regressie in browse-flow of selectie
## 12. Aanbeveling
Aanbevolen v1-richting met laag regressierisico:
- **nu nog geen echte video thumbnails implementeren zonder extra dependency**
- houd de huidige mediaslot-aanpak aan:
- image thumbnails waar al ondersteund
- voor video voorlopig een video-icoon/placeholder
- als de feature later echt gebouwd moet worden:
- gebruik `ffmpeg` als expliciete dependency
- scopeer eerst op `mp4`
- behandel `mkv` als best-effort
- combineer dit met lazy loading
- overweeg pas daarna een eenvoudige cache
Samengevat:
- eerlijke technische conclusie: echte video thumbnails in v1 zonder dependency zijn niet verstandig
- veiligste huidige richting: uitstellen of beperken tot ontwerpvoorbereiding
- als later toch doorgepakt wordt, dan alleen met expliciete keuze voor `ffmpeg` en een smalle, performance-bewuste scope
@@ -135,6 +135,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('paneState("right").currentPath = settingsState.preferredStartupPathRight || "/Volumes";', app_js) self.assertIn('paneState("right").currentPath = settingsState.preferredStartupPathRight || "/Volumes";', app_js)
self.assertIn('"/api/settings"', app_js) self.assertIn('"/api/settings"', app_js)
self.assertIn('`/api/files/thumbnail?', app_js) self.assertIn('`/api/files/thumbnail?', app_js)
self.assertIn("function iconTypeForEntry(entry)", app_js)
self.assertIn("function mediaIconSvg(type)", app_js)
self.assertIn('const iconType = iconTypeForEntry(entry);', app_js)
self.assertIn('function createMediaSlot(entry)', app_js) self.assertIn('function createMediaSlot(entry)', app_js)
self.assertNotIn("select-marker", app_js) self.assertNotIn("select-marker", app_js)
self.assertIn('function openSearch()', app_js) self.assertIn('function openSearch()', app_js)
@@ -179,6 +182,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('.settings-tabs', style_css) self.assertIn('.settings-tabs', style_css)
self.assertIn('.entry-media-slot', style_css) self.assertIn('.entry-media-slot', style_css)
self.assertIn('.entry-media-icon.folder', style_css) self.assertIn('.entry-media-icon.folder', style_css)
self.assertIn('.entry-media-icon.video', style_css)
self.assertIn('.entry-media-icon.pdf', style_css)
self.assertIn('.entry-media-svg', style_css)
self.assertIn('.entry-media-icon.file', style_css) self.assertIn('.entry-media-icon.file', style_css)
self.assertNotIn('.select-marker', style_css) self.assertNotIn('.select-marker', style_css)
+85 -4
View File
@@ -389,6 +389,87 @@ function isThumbnailCandidate(entry) {
return lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".png") || lower.endsWith(".webp"); return lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".png") || lower.endsWith(".webp");
} }
function iconTypeForEntry(entry) {
if (!entry) {
return "file";
}
if (entry.kind === "directory") {
return "folder";
}
const name = entry.name || "";
const lower = name.toLowerCase();
if (lower === "dockerfile" || lower === "containerfile") {
return "docker";
}
if (["jpg", "jpeg", "png", "webp", "gif", "bmp", "avif"].some((ext) => lower.endsWith(`.${ext}`))) {
return "image";
}
if (["mp4", "mkv", "mov", "avi", "webm"].some((ext) => lower.endsWith(`.${ext}`))) {
return "video";
}
if (lower.endsWith(".pdf")) {
return "pdf";
}
if (["md", "markdown"].some((ext) => lower.endsWith(`.${ext}`))) {
return "markdown";
}
if (lower.endsWith(".json")) {
return "json";
}
if (["yaml", "yml"].some((ext) => lower.endsWith(`.${ext}`))) {
return "yaml";
}
if (lower.endsWith(".css")) {
return "css";
}
if (["js", "mjs", "cjs"].some((ext) => lower.endsWith(`.${ext}`))) {
return "javascript";
}
if (["ts", "tsx"].some((ext) => lower.endsWith(`.${ext}`))) {
return "typescript";
}
if (["html", "htm"].some((ext) => lower.endsWith(`.${ext}`))) {
return "html";
}
if (lower.endsWith(".xml")) {
return "xml";
}
if (["sh", "bash", "zsh", "fish"].some((ext) => lower.endsWith(`.${ext}`))) {
return "shell";
}
if (lower.endsWith(".py")) {
return "python";
}
if (["txt", "log", "ini", "cfg", "conf"].some((ext) => lower.endsWith(`.${ext}`))) {
return "text";
}
return "file";
}
function mediaIconSvg(type) {
const icons = {
folder: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M3.5 7.5a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v7a2.5 2.5 0 0 1-2.5 2.5H6a2.5 2.5 0 0 1-2.5-2.5z"/></svg>',
file: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M13 3.5V8h4.5"/></svg>',
image: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><circle cx="9" cy="10" r="1.6"/><path d="M6.5 17l4.2-4.3 2.8 2.8 1.9-2L18.5 17z"/></svg>',
video: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><rect x="4" y="6" width="11" height="12" rx="2"/><path d="M16 10.2l4-2.2v8l-4-2.2z"/></svg>',
pdf: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8 16h8"/><path d="M8.2 12.4h2.1a1.4 1.4 0 1 0 0-2.8H8.2z"/><path d="M12.2 9.6v5.6h1.6a1.9 1.9 0 0 0 0-3.8h-1.6"/><path d="M17 9.6h-2.6v5.6"/><path d="M14.8 12.2h1.7"/></svg>',
text: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M9 10h6"/><path d="M9 13h6"/><path d="M9 16h4"/></svg>',
markdown: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8.5 15v-4l1.8 2 1.7-2v4"/><path d="M13.6 11.2h2.6"/><path d="M14.9 10.2v5.2"/><path d="M13.9 14.5l1 1 1-1"/></svg>',
json: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M10 9c-.9.6-1.3 1.5-1.3 3s.4 2.4 1.3 3"/><path d="M14 9c.9.6 1.3 1.5 1.3 3s-.4 2.4-1.3 3"/><circle cx="12" cy="12" r=".8" fill="currentColor" stroke="none"/></svg>',
yaml: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8.5 9.5l2 2.5v3"/><path d="M12 9.5l2 2.5"/><path d="M14.5 12.8V15"/><path d="M8.5 16h7"/></svg>',
css: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M14.5 9.8a2.4 2.4 0 1 0 0 4.8"/><path d="M9.5 9.8a2.4 2.4 0 1 1 0 4.8"/></svg>',
javascript: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M10.4 9.7v4.8c0 1-.5 1.5-1.4 1.5"/><path d="M13.8 9.8c1 0 1.8.4 1.8 1.3 0 .8-.6 1.1-1.5 1.3-.8.2-1.4.4-1.4 1.2 0 .7.6 1.2 1.6 1.2"/></svg>',
typescript: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M8.5 9.8h4"/><path d="M10.5 9.8v5.4"/><path d="M14.2 9.8c.9 0 1.7.4 1.7 1.3 0 .8-.6 1.1-1.4 1.3-.8.2-1.4.4-1.4 1.2 0 .7.6 1.2 1.6 1.2"/></svg>',
html: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M9.5 10.2l-1.7 1.8 1.7 1.8"/><path d="M14.5 10.2l1.7 1.8-1.7 1.8"/><path d="M13.1 9.5l-2.2 5"/></svg>',
xml: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 3.5h6l4 4v12a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 6 19.5V5A1.5 1.5 0 0 1 7.5 3.5z"/><path d="M9.4 10.3l-1.5 1.7 1.5 1.7"/><path d="M14.6 10.3l1.5 1.7-1.5 1.7"/><path d="M11.8 9.5l.7 5"/></svg>',
shell: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><rect x="4" y="6" width="16" height="12" rx="2"/><path d="M8 10l2 2-2 2"/><path d="M12 15h4"/></svg>',
python: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M12 5c-3.8 0-3.6 1.7-3.6 1.7V9h7.3V6.7S15.8 5 12 5z"/><circle cx="10" cy="7" r=".8" fill="currentColor" stroke="none"/><path d="M12 19c3.8 0 3.6-1.7 3.6-1.7V15H8.3v2.3S8.2 19 12 19z"/><circle cx="14" cy="17" r=".8" fill="currentColor" stroke="none"/><path d="M8.3 9v3H7c-1.8 0-1.8 1.7-1.8 1.7S5 17 8.7 17"/><path d="M15.7 15v-3H17c1.8 0 1.8-1.7 1.8-1.7S19 7 15.3 7"/></svg>',
docker: '<svg viewBox="0 0 24 24" class="entry-media-svg" aria-hidden="true"><path d="M7 10h2v2H7z"/><path d="M10 10h2v2h-2z"/><path d="M13 10h2v2h-2z"/><path d="M10 7.5h2V10h-2z"/><path d="M13 7.5h2V10h-2z"/><path d="M16.2 10.7c.4-1 .3-2-.3-2.8.9-.1 1.8.2 2.4.8.6.6.9 1.4.8 2.3.7.1 1.3.7 1.6 1.3-.4.4-1 .7-1.6.8-.5 2.3-2.3 3.8-5 3.8H9.2c-2.2 0-4-1.8-4-4v-1.2h11z"/></svg>',
};
return icons[type] || icons.file;
}
function createMediaSlot(entry) { function createMediaSlot(entry) {
const slot = document.createElement("span"); const slot = document.createElement("span");
slot.className = "entry-media-slot"; slot.className = "entry-media-slot";
@@ -403,8 +484,10 @@ function createMediaSlot(entry) {
} }
const icon = document.createElement("span"); const icon = document.createElement("span");
icon.className = `entry-media-icon ${entry.kind === "directory" ? "folder" : "file"}`; const iconType = iconTypeForEntry(entry);
icon.className = `entry-media-icon ${iconType}`;
icon.setAttribute("aria-hidden", "true"); icon.setAttribute("aria-hidden", "true");
icon.innerHTML = mediaIconSvg(iconType);
slot.append(icon); slot.append(icon);
return slot; return slot;
} }
@@ -714,9 +797,7 @@ function renderPaneItems(pane) {
}; };
const upNameCell = document.createElement("span"); const upNameCell = document.createElement("span");
upNameCell.className = "entry-name entry-dir"; upNameCell.className = "entry-name entry-dir";
const upMedia = document.createElement("span"); upNameCell.append(createMediaSlot({ name: "..", path: entry.path, kind: "directory" }));
upMedia.className = "entry-media-slot";
upNameCell.append(upMedia);
const upName = document.createElement("button"); const upName = document.createElement("button");
upName.type = "button"; upName.type = "button";
upName.className = "dir-link"; upName.className = "dir-link";
+47 -46
View File
@@ -347,60 +347,61 @@ button:disabled {
} }
.entry-media-icon { .entry-media-icon {
width: 14px; width: 18px;
height: 14px; height: 18px;
position: relative; display: inline-flex;
display: inline-block; align-items: center;
justify-content: center;
color: var(--color-text-muted);
} }
.entry-media-icon.folder::before { .entry-media-svg {
content: ""; width: 18px;
position: absolute; height: 18px;
left: 1px; display: block;
top: 4px; fill: none;
width: 12px; stroke: currentColor;
height: 8px; stroke-width: 1.55;
border: 1.5px solid var(--color-text-muted); stroke-linecap: round;
border-radius: 2px; stroke-linejoin: round;
background: transparent;
} }
.entry-media-icon.folder::after { .entry-media-icon.folder {
content: ""; color: color-mix(in srgb, #d1a85e 72%, var(--color-text-muted));
position: absolute;
left: 2px;
top: 2px;
width: 5px;
height: 3px;
border: 1.5px solid var(--color-text-muted);
border-bottom: 0;
border-radius: 2px 2px 0 0;
background: transparent;
} }
.entry-media-icon.file::before { .entry-media-icon.file {
content: ""; color: var(--color-text-muted);
position: absolute;
left: 2px;
top: 1px;
width: 9px;
height: 12px;
border: 1.5px solid var(--color-text-muted);
border-radius: 2px;
background: transparent;
} }
.entry-media-icon.file::after { .entry-media-icon.image {
content: ""; color: color-mix(in srgb, #62a58c 70%, var(--color-text-muted));
position: absolute; }
right: 1px;
top: 1px; .entry-media-icon.video {
width: 4px; color: color-mix(in srgb, #7f8ed1 70%, var(--color-text-muted));
height: 4px; }
background: var(--color-button-secondary-bg);
border-top: 1.5px solid var(--color-text-muted); .entry-media-icon.pdf {
border-right: 1.5px solid var(--color-text-muted); color: color-mix(in srgb, #d06b6b 70%, var(--color-text-muted));
transform: skew(-12deg, -12deg); }
.entry-media-icon.markdown,
.entry-media-icon.text,
.entry-media-icon.yaml,
.entry-media-icon.xml {
color: color-mix(in srgb, #8ea0b8 82%, var(--color-text-muted));
}
.entry-media-icon.json,
.entry-media-icon.javascript,
.entry-media-icon.typescript,
.entry-media-icon.css,
.entry-media-icon.html,
.entry-media-icon.shell,
.entry-media-icon.python,
.entry-media-icon.docker {
color: color-mix(in srgb, var(--color-accent) 60%, var(--color-text-muted));
} }
.entry-label { .entry-label {