feat: iconenen aangepast
This commit is contained in:
@@ -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, video’s, pdf’s 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 risico’s:
|
||||
- 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
|
||||
@@ -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 video’s 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 video’s 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
|
||||
Binary file not shown.
@@ -135,6 +135,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('paneState("right").currentPath = settingsState.preferredStartupPathRight || "/Volumes";', app_js)
|
||||
self.assertIn('"/api/settings"', 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.assertNotIn("select-marker", app_js)
|
||||
self.assertIn('function openSearch()', app_js)
|
||||
@@ -179,6 +182,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('.settings-tabs', style_css)
|
||||
self.assertIn('.entry-media-slot', 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.assertNotIn('.select-marker', style_css)
|
||||
|
||||
|
||||
+85
-4
@@ -389,6 +389,87 @@ function isThumbnailCandidate(entry) {
|
||||
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) {
|
||||
const slot = document.createElement("span");
|
||||
slot.className = "entry-media-slot";
|
||||
@@ -403,8 +484,10 @@ function createMediaSlot(entry) {
|
||||
}
|
||||
|
||||
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.innerHTML = mediaIconSvg(iconType);
|
||||
slot.append(icon);
|
||||
return slot;
|
||||
}
|
||||
@@ -714,9 +797,7 @@ function renderPaneItems(pane) {
|
||||
};
|
||||
const upNameCell = document.createElement("span");
|
||||
upNameCell.className = "entry-name entry-dir";
|
||||
const upMedia = document.createElement("span");
|
||||
upMedia.className = "entry-media-slot";
|
||||
upNameCell.append(upMedia);
|
||||
upNameCell.append(createMediaSlot({ name: "..", path: entry.path, kind: "directory" }));
|
||||
const upName = document.createElement("button");
|
||||
upName.type = "button";
|
||||
upName.className = "dir-link";
|
||||
|
||||
+47
-46
@@ -347,60 +347,61 @@ button:disabled {
|
||||
}
|
||||
|
||||
.entry-media-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.entry-media-icon.folder::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
top: 4px;
|
||||
width: 12px;
|
||||
height: 8px;
|
||||
border: 1.5px solid var(--color-text-muted);
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
.entry-media-svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.55;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.entry-media-icon.folder::after {
|
||||
content: "";
|
||||
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.folder {
|
||||
color: color-mix(in srgb, #d1a85e 72%, var(--color-text-muted));
|
||||
}
|
||||
|
||||
.entry-media-icon.file::before {
|
||||
content: "";
|
||||
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 {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.entry-media-icon.file::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: var(--color-button-secondary-bg);
|
||||
border-top: 1.5px solid var(--color-text-muted);
|
||||
border-right: 1.5px solid var(--color-text-muted);
|
||||
transform: skew(-12deg, -12deg);
|
||||
.entry-media-icon.image {
|
||||
color: color-mix(in srgb, #62a58c 70%, var(--color-text-muted));
|
||||
}
|
||||
|
||||
.entry-media-icon.video {
|
||||
color: color-mix(in srgb, #7f8ed1 70%, var(--color-text-muted));
|
||||
}
|
||||
|
||||
.entry-media-icon.pdf {
|
||||
color: color-mix(in srgb, #d06b6b 70%, var(--color-text-muted));
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
Reference in New Issue
Block a user