From 8e3f783780b012dd08941cb8f5e9c0913fcaeaeb Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Tue, 2 Jun 2026 13:02:51 +0300 Subject: [PATCH] feat(handlers): add file icons with standard and retro variants Introduce file icon support to the file browser. Icons are loaded on startup and mapped based on file name and content type. - Load file icon mappings in the App handler initialization. - Add `HasThumbnail`, `IconURL`, and `IconRetroURL` to the file view. - Update CSS to support displaying file icons alongside thumbnails. - Add retro theme support to swap standard icons with pixelated retro variants when the retro theme is active. --- backend/libs/handlers/app.go | 6 + backend/libs/handlers/download.go | 7 + backend/libs/handlers/icons.go | 152 ++++++++++++++++++ backend/libs/handlers/icons_test.go | 54 +++++++ backend/static/css/30-download.css | 101 ++++++++---- backend/static/file-icons/icon-map.json | 112 +++++++++++++ .../file-icons/retro/mshtml.dll_14_2660-2.png | Bin 0 -> 1787 bytes .../file-icons/retro/shell32.dll_14_151-2.png | Bin 0 -> 386 bytes .../file-icons/retro/shell32.dll_14_152-2.png | Bin 0 -> 2121 bytes .../file-icons/retro/shell32.dll_14_2-0.png | Bin 0 -> 553 bytes .../file-icons/retro/shell32.dll_14_3-0.png | Bin 0 -> 378 bytes .../file-icons/retro/shimgvw.dll_14_1-2.png | Bin 0 -> 1671 bytes .../file-icons/retro/wmploc.dll_14_504-2.png | Bin 0 -> 594 bytes .../file-icons/retro/wmploc.dll_14_610-2.png | Bin 0 -> 621 bytes .../file-icons/retro/zipfldr.dll_14_101-2.png | Bin 0 -> 598 bytes .../standard/audio-document-svgrepo-com.svg | 10 ++ .../standard/csv-document-svgrepo-com.svg | 10 ++ .../standard/excel-document-svgrepo-com.svg | 2 + .../standard/exe-document-svgrepo-com.svg | 10 ++ .../standard/flash-document-svgrepo-com.svg | 10 ++ .../standard/html-document-svgrepo-com.svg | 10 ++ .../standard/image-document-svgrepo-com.svg | 10 ++ .../standard/mp4-document-svgrepo-com.svg | 10 ++ .../standard/pages-document-svgrepo-com.svg | 10 ++ .../standard/pdf-document-svgrepo-com.svg | 16 ++ .../standard/psd-document-svgrepo-com.svg | 10 ++ .../standard/rtf-document-svgrepo-com.svg | 10 ++ .../standard/txt-document-svgrepo-com.svg | 10 ++ .../standard/video-document-svgrepo-com.svg | 10 ++ .../standard/visio-document-svgrepo-com.svg | 10 ++ .../standard/word-document-svgrepo-com.svg | 10 ++ .../standard/zip-document-svgrepo-com.svg | 10 ++ backend/templates/pages/download.html | 47 +++--- 33 files changed, 594 insertions(+), 53 deletions(-) create mode 100644 backend/libs/handlers/icons.go create mode 100644 backend/libs/handlers/icons_test.go create mode 100644 backend/static/file-icons/icon-map.json create mode 100644 backend/static/file-icons/retro/mshtml.dll_14_2660-2.png create mode 100644 backend/static/file-icons/retro/shell32.dll_14_151-2.png create mode 100644 backend/static/file-icons/retro/shell32.dll_14_152-2.png create mode 100644 backend/static/file-icons/retro/shell32.dll_14_2-0.png create mode 100644 backend/static/file-icons/retro/shell32.dll_14_3-0.png create mode 100644 backend/static/file-icons/retro/shimgvw.dll_14_1-2.png create mode 100644 backend/static/file-icons/retro/wmploc.dll_14_504-2.png create mode 100644 backend/static/file-icons/retro/wmploc.dll_14_610-2.png create mode 100644 backend/static/file-icons/retro/zipfldr.dll_14_101-2.png create mode 100644 backend/static/file-icons/standard/audio-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/csv-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/excel-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/exe-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/flash-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/html-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/image-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/mp4-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/pages-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/pdf-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/psd-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/rtf-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/txt-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/video-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/visio-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/word-document-svgrepo-com.svg create mode 100644 backend/static/file-icons/standard/zip-document-svgrepo-com.svg diff --git a/backend/libs/handlers/app.go b/backend/libs/handlers/app.go index 824829f..c982e72 100644 --- a/backend/libs/handlers/app.go +++ b/backend/libs/handlers/app.go @@ -20,9 +20,14 @@ type App struct { banService *services.BanService rateLimiter *rateLimiter uploadGroups *uploadGrouper + fileIcons *fileIconSet } func NewApp(cfg config.Config, logger *slog.Logger, renderer *web.Renderer, uploadService *services.UploadService, authService *services.AuthService, settingsService *services.SettingsService, reactionService *services.ReactionService, banService *services.BanService) *App { + fileIcons, err := loadFileIcons(cfg.StaticDir) + if err != nil { + logger.Warn("failed to load file icon map", "source", "handlers", "severity", "warn", "error", err.Error()) + } return &App{ cfg: cfg, logger: logger, @@ -34,6 +39,7 @@ func NewApp(cfg config.Config, logger *slog.Logger, renderer *web.Renderer, uplo banService: banService, rateLimiter: newRateLimiter(), uploadGroups: newUploadGrouper(), + fileIcons: fileIcons, } } diff --git a/backend/libs/handlers/download.go b/backend/libs/handlers/download.go index 780e616..7751c4b 100644 --- a/backend/libs/handlers/download.go +++ b/backend/libs/handlers/download.go @@ -45,6 +45,9 @@ type fileView struct { URL string DownloadURL string ThumbnailURL string + HasThumbnail bool + IconURL string + IconRetroURL string ReactURL string Reactions []reactionView Reacted bool @@ -350,6 +353,7 @@ func (a *App) fileView(box services.Box, file services.File) fileView { } func (a *App) fileViewWithReactions(box services.Box, file services.File, reactions []services.ReactionSummary, reacted bool) fileView { + icon := a.fileIcons.lookup(file.Name, file.ContentType) return fileView{ ID: file.ID, Name: file.Name, @@ -359,6 +363,9 @@ func (a *App) fileViewWithReactions(box services.Box, file services.File, reacti URL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID), DownloadURL: fmt.Sprintf("/d/%s/f/%s/download", box.ID, file.ID), ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID), + HasThumbnail: file.Thumbnail != "", + IconURL: fileIconURL("standard", icon.Standard), + IconRetroURL: fileIconURL("retro", icon.Retro), ReactURL: fmt.Sprintf("/d/%s/f/%s/react", box.ID, file.ID), Reactions: a.reactionViews(reactions), Reacted: reacted, diff --git a/backend/libs/handlers/icons.go b/backend/libs/handlers/icons.go new file mode 100644 index 0000000..b1e6415 --- /dev/null +++ b/backend/libs/handlers/icons.go @@ -0,0 +1,152 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" +) + +// fileIcon holds the two icon filenames for a file type: the standard (modern) +// icon and the retro (Win98) icon. The filenames are resolved against +// static/file-icons/standard and static/file-icons/retro respectively. +type fileIcon struct { + Standard string `json:"standard"` + Retro string `json:"retro"` +} + +type iconType struct { + Mime string `json:"mime"` + Standard string `json:"standard"` + Retro string `json:"retro"` + Extensions []string `json:"extensions"` +} + +type iconMapFile struct { + Default iconType `json:"default"` + Types []iconType `json:"types"` +} + +type mimeRule struct { + pattern string // exact mime ("application/pdf") or major prefix ("image/") + prefix bool + icon fileIcon +} + +// fileIconSet is the loaded icon map: an extension lookup plus content-type +// rules and a fallback. It is built once at startup from icon-map.json. +type fileIconSet struct { + byExt map[string]fileIcon + byMime []mimeRule + fallback fileIcon +} + +// loadFileIcons reads static/file-icons/icon-map.json and indexes it by +// extension and content type so icons can be assigned at render time. +func loadFileIcons(staticDir string) (*fileIconSet, error) { + data, err := os.ReadFile(filepath.Join(staticDir, "file-icons", "icon-map.json")) + if err != nil { + return nil, err + } + var raw iconMapFile + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + set := &fileIconSet{ + byExt: make(map[string]fileIcon), + fallback: fileIcon{Standard: raw.Default.Standard, Retro: raw.Default.Retro}, + } + if err := validateFileIcon(staticDir, set.fallback); err != nil { + return nil, err + } + for _, t := range raw.Types { + icon := fileIcon{Standard: t.Standard, Retro: t.Retro} + if err := validateFileIcon(staticDir, icon); err != nil { + return nil, err + } + for _, ext := range t.Extensions { + set.byExt[strings.ToLower(strings.TrimPrefix(ext, "."))] = icon + } + if t.Mime == "" { + continue + } + if strings.HasSuffix(t.Mime, "/*") { + set.byMime = append(set.byMime, mimeRule{pattern: strings.TrimSuffix(t.Mime, "*"), prefix: true, icon: icon}) + } else { + set.byMime = append(set.byMime, mimeRule{pattern: strings.ToLower(t.Mime), icon: icon}) + } + } + return set, nil +} + +func validateFileIcon(staticDir string, icon fileIcon) error { + if icon.Standard != "" { + if err := validateFileIconPath(staticDir, "standard", icon.Standard); err != nil { + return err + } + } + if icon.Retro != "" { + if err := validateFileIconPath(staticDir, "retro", icon.Retro); err != nil { + return err + } + } + return nil +} + +func validateFileIconPath(staticDir, theme, name string) error { + if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") { + return fmt.Errorf("invalid %s file icon path %q", theme, name) + } + path := filepath.Join(staticDir, "file-icons", theme, name) + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("missing %s file icon %q: %w", theme, name, err) + } + if info.IsDir() { + return fmt.Errorf("%s file icon %q is a directory", theme, name) + } + return nil +} + +// lookup resolves a file's icon from its name (extension) first, falling back to +// its content type, then to the default icon. Extension wins because stored +// content types are often the generic application/octet-stream. +func (s *fileIconSet) lookup(name, contentType string) fileIcon { + if s == nil { + return fileIcon{} + } + if ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), ".")); ext != "" { + if icon, ok := s.byExt[ext]; ok { + return icon + } + } + + ct := strings.ToLower(strings.TrimSpace(contentType)) + if i := strings.IndexByte(ct, ';'); i >= 0 { + ct = strings.TrimSpace(ct[:i]) + } + if ct != "" && ct != "application/octet-stream" { + for _, rule := range s.byMime { // exact matches first + if !rule.prefix && rule.pattern == ct { + return rule.icon + } + } + for _, rule := range s.byMime { // then major-type prefixes + if rule.prefix && strings.HasPrefix(ct, rule.pattern) { + return rule.icon + } + } + } + return s.fallback +} + +// fileIconURL builds the /static URL for an icon filename in the given theme +// directory ("standard" or "retro"). +func fileIconURL(theme, name string) string { + if name == "" { + return "" + } + return "/static/file-icons/" + theme + "/" + name +} diff --git a/backend/libs/handlers/icons_test.go b/backend/libs/handlers/icons_test.go new file mode 100644 index 0000000..039566f --- /dev/null +++ b/backend/libs/handlers/icons_test.go @@ -0,0 +1,54 @@ +package handlers + +import ( + "path/filepath" + "testing" +) + +func TestFileIconMapLoadsAndResolvesCommonTypes(t *testing.T) { + icons, err := loadFileIcons(filepath.Join("..", "..", "static")) + if err != nil { + t.Fatalf("loadFileIcons returned error: %v", err) + } + + tests := []struct { + name string + contentType string + wantStandard string + wantRetro string + }{ + { + name: "photo.jpg", + contentType: "application/octet-stream", + wantStandard: "image-document-svgrepo-com.svg", + wantRetro: "shimgvw.dll_14_1-2.png", + }, + { + name: "movie.mkv", + contentType: "", + wantStandard: "video-document-svgrepo-com.svg", + wantRetro: "wmploc.dll_14_504-2.png", + }, + { + name: "archive.7z", + contentType: "", + wantStandard: "zip-document-svgrepo-com.svg", + wantRetro: "zipfldr.dll_14_101-2.png", + }, + { + name: "unknown.bin", + contentType: "application/octet-stream", + wantStandard: "txt-document-svgrepo-com.svg", + wantRetro: "shell32.dll_14_152-2.png", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := icons.lookup(tt.name, tt.contentType) + if got.Standard != tt.wantStandard || got.Retro != tt.wantRetro { + t.Fatalf("lookup returned %+v, want standard=%q retro=%q", got, tt.wantStandard, tt.wantRetro) + } + }) + } +} diff --git a/backend/static/css/30-download.css b/backend/static/css/30-download.css index 31ca5dd..21755aa 100644 --- a/backend/static/css/30-download.css +++ b/backend/static/css/30-download.css @@ -65,7 +65,7 @@ .file-card { position: relative; - padding-bottom: 2.6rem; + padding-block: 0.65rem 2.6rem; } .file-reaction-dock { @@ -303,24 +303,60 @@ html.reaction-picker-open body { object-fit: contain; } -.thumb-link { - display: block; +/* A file row behaves like an entry in a desktop file explorer: a small + thumbnail/icon followed by the name and metadata. The whole row is the click + target (raw view of the file). */ +.file-open { + min-width: 0; + flex: 1; + display: flex; + align-items: center; + gap: 0.8rem; + color: var(--foreground); + text-decoration: none; +} + +.file-media { + flex: 0 0 3rem; + width: 3rem; + height: 3rem; + display: grid; + place-items: center; overflow: hidden; - flex: 0 0 4.75rem; - width: 4.75rem; - aspect-ratio: 16 / 10; border: 1px solid var(--border); border-radius: calc(var(--radius) - 0.125rem); background: var(--muted); } -.thumb-link img { +.file-thumb { width: 100%; height: 100%; display: block; object-fit: cover; } +.file-icon { + width: 2.1rem; + height: 2.1rem; + display: block; + object-fit: contain; +} + +/* Retro (Win98) icons are tiny pixel art — keep them crisp and swap them in + only when the retro theme is active. */ +.file-icon-retro { + display: none; + image-rendering: pixelated; +} + +[data-theme="retro"] .file-icon-standard { + display: none; +} + +[data-theme="retro"] .file-icon-retro { + display: block; +} + .file-main { min-width: 0; max-width: 100%; @@ -329,46 +365,47 @@ html.reaction-picker-open body { text-decoration: none; } -.file-actions { - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.preview-action [hidden] { - display: none; +.file-main small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .file-browser.is-thumbs { - grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); } .file-browser.is-thumbs .file-card { display: grid; min-width: 0; align-content: start; - gap: 0.7rem; + gap: 0.5rem; +} + +.file-browser.is-thumbs .file-open { + display: grid; + gap: 0.55rem; + text-align: center; + justify-items: center; +} + +.file-browser.is-thumbs .file-media { + width: 100%; + height: auto; + flex-basis: auto; + aspect-ratio: 16 / 10; +} + +.file-browser.is-thumbs .file-icon { + width: 45%; + height: auto; } .file-browser.is-thumbs .file-main { width: 100%; } -.file-browser.is-thumbs .thumb-link { - width: 100%; - flex-basis: auto; -} - -.file-browser.is-thumbs .button { - width: 100%; -} - -.file-browser.is-thumbs .file-actions { - width: 100%; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - .file-browser.images-only .file-card:not([data-kind="image"]) { display: none; } diff --git a/backend/static/file-icons/icon-map.json b/backend/static/file-icons/icon-map.json new file mode 100644 index 0000000..e5e783b --- /dev/null +++ b/backend/static/file-icons/icon-map.json @@ -0,0 +1,112 @@ +{ + "_comment": "Maps a file's type (resolved from its extension / content type) to a file-type icon. 'standard' icons live in file-icons/standard, 'retro' (Win98) icons in file-icons/retro. The server reads this at startup and picks the icon per file; thumbnails always win over icons when present.", + "default": { + "mime": "application/octet-stream", + "standard": "txt-document-svgrepo-com.svg", + "retro": "shell32.dll_14_152-2.png" + }, + "types": [ + { + "mime": "image/*", + "standard": "image-document-svgrepo-com.svg", + "retro": "shimgvw.dll_14_1-2.png", + "extensions": ["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "ico", "tif", "tiff", "heic", "heif", "avif", "jfif"] + }, + { + "mime": "image/vnd.adobe.photoshop", + "standard": "psd-document-svgrepo-com.svg", + "retro": "shimgvw.dll_14_1-2.png", + "extensions": ["psd"] + }, + { + "mime": "audio/*", + "standard": "audio-document-svgrepo-com.svg", + "retro": "wmploc.dll_14_610-2.png", + "extensions": ["mp3", "wav", "flac", "aac", "ogg", "oga", "m4a", "wma", "opus", "aiff", "aif", "mid", "midi"] + }, + { + "mime": "video/mp4", + "standard": "mp4-document-svgrepo-com.svg", + "retro": "wmploc.dll_14_504-2.png", + "extensions": ["mp4", "m4v"] + }, + { + "mime": "video/*", + "standard": "video-document-svgrepo-com.svg", + "retro": "wmploc.dll_14_504-2.png", + "extensions": ["mkv", "mov", "avi", "webm", "wmv", "flv", "mpg", "mpeg", "3gp", "ogv", "ts", "m2ts"] + }, + { + "mime": "application/zip", + "standard": "zip-document-svgrepo-com.svg", + "retro": "zipfldr.dll_14_101-2.png", + "extensions": ["zip", "rar", "7z", "gz", "tar", "bz2", "xz", "tgz", "zst", "lz", "lzma", "cab", "iso"] + }, + { + "mime": "application/pdf", + "standard": "pdf-document-svgrepo-com.svg", + "retro": "shell32.dll_14_152-2.png", + "extensions": ["pdf"] + }, + { + "mime": "text/html", + "standard": "html-document-svgrepo-com.svg", + "retro": "mshtml.dll_14_2660-2.png", + "extensions": ["html", "htm", "xhtml", "mhtml"] + }, + { + "mime": "application/x-shockwave-flash", + "standard": "flash-document-svgrepo-com.svg", + "retro": "shell32.dll_14_152-2.png", + "extensions": ["swf", "fla"] + }, + { + "mime": "application/vnd.ms-excel", + "standard": "excel-document-svgrepo-com.svg", + "retro": "shell32.dll_14_151-2.png", + "extensions": ["xls", "xlsx", "xlsm", "ods"] + }, + { + "mime": "text/csv", + "standard": "csv-document-svgrepo-com.svg", + "retro": "shell32.dll_14_151-2.png", + "extensions": ["csv", "tsv"] + }, + { + "mime": "application/msword", + "standard": "word-document-svgrepo-com.svg", + "retro": "shell32.dll_14_2-0.png", + "extensions": ["doc", "docx", "odt"] + }, + { + "mime": "application/rtf", + "standard": "rtf-document-svgrepo-com.svg", + "retro": "shell32.dll_14_2-0.png", + "extensions": ["rtf"] + }, + { + "mime": "application/vnd.apple.pages", + "standard": "pages-document-svgrepo-com.svg", + "retro": "shell32.dll_14_2-0.png", + "extensions": ["pages"] + }, + { + "mime": "application/vnd.visio", + "standard": "visio-document-svgrepo-com.svg", + "retro": "shell32.dll_14_152-2.png", + "extensions": ["vsd", "vsdx"] + }, + { + "mime": "application/x-msdownload", + "standard": "exe-document-svgrepo-com.svg", + "retro": "shell32.dll_14_3-0.png", + "extensions": ["exe", "msi", "bat", "cmd", "com", "app", "dmg", "apk", "deb", "rpm", "appimage"] + }, + { + "mime": "text/plain", + "standard": "txt-document-svgrepo-com.svg", + "retro": "shell32.dll_14_151-2.png", + "extensions": ["txt", "text", "log", "md", "markdown", "ini", "cfg", "conf", "json", "xml", "yaml", "yml", "toml", "js", "ts", "jsx", "tsx", "go", "py", "rb", "php", "java", "c", "h", "cpp", "cc", "cs", "rs", "sh", "bash", "css", "scss", "sql"] + } + ] +} diff --git a/backend/static/file-icons/retro/mshtml.dll_14_2660-2.png b/backend/static/file-icons/retro/mshtml.dll_14_2660-2.png new file mode 100644 index 0000000000000000000000000000000000000000..f91d7516a75cc3aeeb163a4e7089dc16b48b9681 GIT binary patch literal 1787 zcmV004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x1`Sy97h<&pA(zLxY;WLfdtGshB#mj{Gwn4 zCLjcE-5f4Lnk!ZW;mVLBe?hDwRk%U}ad(9Y32q&(2>~I5UlTPl=E!1jh>bzsP8hOb zQ_RlS?#$la=|CPZ%k9_vp7(3s9k~-l5#Fbf4-bmawdlc(PVL-z1KZF90st`BTqH90 zLEdKz>71treBeGSJI{>&Ex_i!a}lr%cu2&`>6>C$4F$doJOD&HA9U0_EAc=T+6D}( zka5mdxG&{}RY5NT&gGp90+qi3f0E7{3k%Tk%K%%vbKy&igAFvnz{`7#khUV$7J={g z4+?^q3r{3)cVpthGf7zMVGUo^PK11TP_)UX);ZAc9~4XsVhY0F-I(CgrH}FA#ozdG z|0@Sxi>Sc@V9!wv_9wZ@fmR;K^RXA+w)hX<{*GI>uH*T|=h)x>tOl>GAAnD)Vk+LK zO6b9VmRImi3y~}g^8O^Dpa&GQ_C#s9 z#&%l8z8a%Ziou`K)-KlZIUpOM9Su}b}+LAzrw+;rEL-Fd?*JT0V?)4 zHy@fWIWC(nW(eL}J-kQ6HrK0D$OE-1w7K~ZckXt*X0Al?5*XKAnIc>$!rU7dd zVTF;ZO00^SE?%TN)|!Cz^>w5v@Wu6yz?U-+1BdA=OtbrQ=WlGOt9_CG1xADlC$@6K z6Gl!5jYcW5Y>HlQio>U`Kn(nx#$I^#1!PNjwHj0P9c+#waI6kTK$cBGBp0bcFAE|r z1P?e(o%)EWeazG+3Aflj0r&3RldB{Hal8cvBTZxMR;<4;{2UR@&s2_00bfGQ$ZF;ok~#Gn@MW(Y(NcKpp;b=yxp08X?AWbarX1X_3{uE7(_919$)USB&t zKGr|tc&nzBnB;Z{%g;)JDC@Tpw|p4c@IZL2(8th1_U_#~#Bu7xzP3KI*$z=*)Bvr# z(Bz4HI4%sldWxL#=D@OqmrSg~2e7J8mh}Lq`lxSVVP0H@k;w%)x!LVBA{dN(I1Y%Q z=50wm-~8QHoda>4)-Il&er@fEK!=gZ1!JaaC`p^DZ>#D*`<@mX!th()@c`pVz*zMS zDsx)pM4~6D?US|yTy`{mQqE?x&ehXD+-}nHf{aU5mCl~jp%JqFTmkJxP;J6h32A}{ zfT-K;;_chFm4}4$z-%__T)l$g8Nm{PvTh5BuXDr1xh-PRP_o;XqlWfmZ8Dj(>Rq%a z)FM`Tf&~GbINOgZr0fN+tyk22#0tK7mt(9CADP80!GPEU(x7j9aAxDbFKaALOL z<&ms~PfGJutD3i3fI9-Z-7fyYHh^}!4l1NhPELRiAK?vd>o0Jin4+^k_ zRChB705iQ8;A;F2HnZ3PTOXYF0000bbVXQnWMOn=I%9HWVRU5xGB7eSEif@HFf&v! zH##*iIy5yaFfckWFq^;e=>Px#C3HntbYx+4WjbwdWNBu305UK#GA%GMEif}wF*iCj dFgi3fD=;uRFfi)kNmT#<002ovPDHLkV1lqKFZ}=j literal 0 HcmV?d00001 diff --git a/backend/static/file-icons/retro/shell32.dll_14_151-2.png b/backend/static/file-icons/retro/shell32.dll_14_151-2.png new file mode 100644 index 0000000000000000000000000000000000000000..d59b1f9a9bdd9dcba8ec8d4b263b547c46083a81 GIT binary patch literal 386 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?e4WlvF3AT~yX`WipnQ%12Yr@P9feshX z0?vsl9&$;c3Qk^|CLNd?!0M48=Ip5;RB->uyM~)d9kpyq(dNIEk60UDt$Jo4vHwNZ z*NfZCcgbr{nZ9!2wP|%gN2->%MwFx^mZVxG7o`Fz1|tI_BV7YST?69~Lvt%r11ked nZ37@_a3geTI*Nwe{FKbJO57S&a_HU$YGCkm^>bP0l+XkK^aq3s literal 0 HcmV?d00001 diff --git a/backend/static/file-icons/retro/shell32.dll_14_152-2.png b/backend/static/file-icons/retro/shell32.dll_14_152-2.png new file mode 100644 index 0000000000000000000000000000000000000000..1af6c1ebdf6d4b2c0162b8c5d563a9a6e7b994e9 GIT binary patch literal 2121 zcmV-P2)6f$P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x2VhA=K~!ko?U`+CQ|A@Of6sNAKuE9$YT81Q>(BvRA&FP1 zEggwtHnf(FzU)KkB!&gjO1G|*e3_t?;2YI8Bw*E~ZXedI(5l;{RxO)o*9}IJA#G|` z)i$llAS8AmG%SsLLkdZ7?%9X?lIxgO*CCEJi6dFI&%Mv%^FQbO&pFRE+>rk>IdU*A zM-IlNl)7R>E7tg1YyLs)t*_zy=#hhQsjBQ{Ud3@&_xG2pnM(D>JB83%KWwQQrGN2;B5ldU}QdjX)#RESw;+ ze6hQ-YHkHp^D5Z9c?AGNL!&GXRMFF0%VUo%XX(-qN8kC7#SGGYekH5!Uq*X-yBs)h zz!wN#DI_AST2;gM;$PwGYwKuUIfs!;Rm9fRv2A;V2OnC(11*CL55UruE7-DS1hW)12y#5{=cO>+^a0tsb<@qx1|s#62x{0WdIdnZU$(M&!GE zXUpRN+;Yo27Sy(ptNSY(pMJ(lr>2`(8~+)Nj}EYY!@8o3_boei9G6#LP05w36{F@Oes*H;B66d?=0cTw^#Vlsa>lWV;kU-8hP&m=qNmpkl>2#WO zZyEq2nKm<{W z)@YPyG>WDaj-wSCA_7VwSO{k$3Pq9FNsVkaODdT}0mFttGLZlv9ByD!JdTM%Qx+dI zRTRxi2VkOV*aBgiymIIee>!~Fz1G&&W?f^l`k4X&MalW#8-RAD5>o_CDSr6WQ#`qC zo15VQ+d%70urf)_3LkvOS4~)0MZ{vTO#0iDNzXf4v9F7>rbOZ!fLT74XOLr*qccSc z`h$p+ygwApF!8=jvry-P$mfFtXr{$K2gWP%2P!&=O4%H+jO{qy0ZNhS?In}8 znqEgo2LO>sg#G*X&kg{u2ymja3xJcUB!*#NWDQbDtDGhiNsMe3fad0Af}s$hP>4t* zf�-%iNZ%~}L34p<4Tl;S6Q_5e_?*Q4uUf}tSK?AnC_bX}(*9L{TUV6D@Xbqt)T z{!mm3xe*ZIkAHsKHqJ6k0ZP+co!FsL%Ho5OHAvdcuggxMSS&^?7MmRaI7T5T@^e3# zNHA;|WYQV>G8q86Iy+D(5{ZPHPEotzHAf;Wn1S>u3V>NYW|I@LMwW-~Uq>pL1fX8m zsn>Nv!61#z%_yb#(Y9>_Lm?W%VRT(bsr*#3(r6X|xIhGg!619~{*;#1s2c@^C!Lf6 zC#~`#Xu3mRXJ;o3VV!U|d|iAfk^=%*WpZ_Et7Vi^PVM`QY?hPBBq)tkGJ%mb$fVO` z(rJurmV`a{>ESTDcJF4(=J0jROVcQ$LMGkE?_PNs1*DQGhK($lbjGdoEzu|ns1NJt zdYH!MCN{-40gAR*6el(_F)tN0jXE5dq%(Z$!S(t1??lW=BTp)6c9TO*76Kd)OfL%l z4>Tk5Eu#hM^m=~$?6bC^tpoq4IqL%rmEa{#%TDFdE- zuC8!DXXq;#fQhQ%0s|Zqg#0nO0Fdc@dLf=Y?3GgJj2Q=@#w)C2K!N9_z@u`Fv%UHFGi*{N|*@eGkx@>)1aWw?!XFI9c`thHHzckYwqe8uBV*Jv7)KbT`yj^DqRii&_!Mx8WrjL8RqOUR1@pD;A|Dd#d7#>U3zIn#}4n*3*AfXd2B zOf!FMD8}e%0f6y>6u5HvGMS!UhCUr+IGd&COgCbh3|_p*+_`hFS=Z;xnZv$)`)2Gh zo$rmdZrxg7O=V@}=iWbEkCqGoz|{Y)X3NMMazjce{{=QiFex_4t;qlY03~!qSaf7z zbY(hYa%Ew3WdJfTGBPbNF)c7NR53R?H846bIV&(QIxsNURh4G|001R)MObuXVRU6W zZEs|0W_bWIFfuYNFflDKGgL7*IyEplFgYtQFgh?WsR%9G00000NkvXXu0mjffj-;& literal 0 HcmV?d00001 diff --git a/backend/static/file-icons/retro/shell32.dll_14_2-0.png b/backend/static/file-icons/retro/shell32.dll_14_2-0.png new file mode 100644 index 0000000000000000000000000000000000000000..ed458f1bebe3e06cdf4819705baafb4be3c16983 GIT binary patch literal 553 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGKAC}m z(ZSQjF(l&f+v(myhZQ*57CzqlC2i8|qmwWC`d-x#a^9t7oFg=KKK}{kg$7DV1|~{} zZc1H`>oA`#gmk|uMk zA(L_VzW2W~=Xx-Rt~@vG zOx4wsvn#b+40&ENojRb*dtT^-BbU_L4ZB&|d_7#QUQm4WP?zy!x|zy?%1e;~5?mL~ z{fceOD_uPGFXs&56)nns#+9DzYb$0udS&_^$k}(a(!J$M<;NJUgGKx`P0#x$rZV1= z%h(vtndkjr@s0L^%-cqdanIIf9=W5wq2aGs1Hjw?^NmZww-;NsZM$0ch40U?T4s-84Gf;HelF{r G5}E+L3efxj literal 0 HcmV?d00001 diff --git a/backend/static/file-icons/retro/shell32.dll_14_3-0.png b/backend/static/file-icons/retro/shell32.dll_14_3-0.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e9146100d2470678ab3388fbd5ef744f2f82bb GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c%7 z(bL5-B;xSf>497g3Oo+;FCKi8@-E|Z%2&l!QNh5o3--#lci26*Q~NVlK`D9lVb>Np zg<~0YJ617?G%6*|=9p+y_Wq9E0;z_F2J004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x1)fPnK~z}7y_a2R990y@e|KiGnPk&+^HEI`tF=kmU?bKq zELI^{1rDY z-Q8p-vvbe&;m&Td$;QTtJ#d+sd(J)QfBxspow;gw_*(;DM9?Bga@ATRaZIc=ajel= zqmySH$M!j~(^?SG7-LwA?Af!Y56A*@o`~?Xa|<}w@gGF3!NweWAK*dYAaKqT5z_12 z{}%aQNYP&UfEEBxj3M1})d5t+Ga{tZ7Qnfj;`O5qOqUclwi?o&VX$4;y-Daw+xiwG z_41m_c#&uU7c)5&*DMrUr zWEzBxlo&sDj=k?32c^JC)n_1#KgIVlY-t+;AY8}+;5TO08kcZB;+q4%aB+MVlp+jc zd+#{V)MSmEl^cyGqB(HtIbw;4O-z5s!!%8-C+%l=`r*A*!$U{kW$5;2>js~hndi#` zr}^=R3$=<$V0ZP}dRl~z#tyX6l%>qn)OXCBm;vDa+aBYVz9ByOb{`*p{u0}JHnU~p zEt3p(w_j(bhUONM#vZQHKQYn-R2?K3Ww z4sq(@BaHra_hmtN+GzHFv7dph1LO+1mEX%^GTelL^S~Au{&ASyx9$dDe0rP>?Hedd znb^d%rpolRenuhsj!7fL0RtBx+mR*HoFSLb)#s|RwMPjXDaZy{ zvO$)E-ydY?_91rOyt8U9M(Ffoy3;?Qjb@F}0pC@$rd-ygTwDcCQhx=UTn<2t0U!JBJ4wS;te3yq&JrNAkSpdO zmoPE~f3-}}+fd-9?(6Uzh36<-W&5u|DO^W!av8;rh}hr5CM=MaWTcb1<|T8@OXkvm zmjrihU*vx;@zh&yGW^>aA}xfmFdqw%HY|vt6tDbQ@r*HxSudshpR%uZ8n795Z0W^K zDf)Z5xnpY|w{Gqw)Pkcd3u2(G4X&f=190o_007*uaWhV(R!QwD4n3t7b!t)PDn%rMYZPT{ zb^?^e;3go&K!mZtfEcraMh_s=QVlK!B5fCG1FG3(3U z1gwuWq$9Mt0000bbVXQnWMOn=I%9HWVRU5xGB7eSEif@HFf&v!H##&qIxsUUFfckW zFhRaPBLDyZC3HntbYx+4WjbwdWNBu305UK#GA%GMEif}wF*iCiIXW;iD=;uRFfcIV RhMxcc002ovPDHLkV1o530!sh@ literal 0 HcmV?d00001 diff --git a/backend/static/file-icons/retro/wmploc.dll_14_504-2.png b/backend/static/file-icons/retro/wmploc.dll_14_504-2.png new file mode 100644 index 0000000000000000000000000000000000000000..4437160e5624cea2502e6eba0f8b5a124c0c1727 GIT binary patch literal 594 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?e4MvptrTjaSK zriAsz?PdP?XJ5|B7bl~BnuWyr3-zZWk`gE$vw2J$F#IV=`r^_hJ=>p;e#iOI?3+x2{lop=59x;-VwtZf2OC7#SED=^7a7 z8W@Kdnp>GzS{WK^8yHv_81#zR?M2a$o1c=IR*74~AI~;Mpaup{S3j3^P6!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?e4>vKGoTDzYuqdldK&7$5gVjmF$+`N%G!|XnK0e>XZVtb< zmMn@Er3uyD3Kk|DW+{=gT$hN(b3H6npBX<{@z71ly)Pfz-s}JWn?ZJ}*VYWH+By%- zt$pWxsu&nfqWk+>GE^@_^w}1 z$=<(FRM$qng|DY z-JNvB&usP|Jq8}O(i?AI1undBiN$yE>76m(O++@ad#g6NC9^&Kn;-&CJFfLypZvE#^ z;Rn)Q-xuqiv^ePFg6ppzecj47JC|>f1l#Gb*-;F}HGB5fvM#>(c+aAe%D%@Bi*ye? zk8ahue*f`Dhla!%*IXKcP6=F!Kb5BcEk4dJTt|$1P1shM?zKm^O#8odA0rdfgcU0k zkM{y&NVUW@q9i4;B-JXpC>2OC7#SED=^7a78W@Kdnp>GzS{WK^8yHv_81#zR?M2a$ bo1c=IR*74~AI~;Mpaup{S3j3^P6NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yLU`z6LcVYMsf(!O8pUl9( znCt1{7?N@CZDioO76l#|vwPfmZhIpwBk#9&pVrby3sqS^d4iPS8w*`mR;zZ+QUS+4 zKesKm&vO-;rZ~7bxwP7G6wLXddd_%r$Ai<$`j{iWb}!KF)P1i0JmVevhQe58p=y6S z-nkwM+n%Xp9ZvI9n3wJK$m7NnCk>$tQ9ove7J-Dd&K!F-F@*buNWI_NJ$;UwQtZ0+ zm(Ro+4n(cdDVA-#RN?U8?@s@_)dwV$8P2>ezNOGIVdGYf-u=h+g-$qZ+3@|8vrDLD z!PL3MuY*fB?&Lc9cuIuV1Hb3Xf9r8vS?}~i>GE8r%-4R6F|WNEb(vx{8dCl@pYr;n z!5ZYg`%L4Ni+bUTp$?)J3x%e0MT&FTY~KP2XO~bm&@1d+L6EIMH%L&-XhUwH9!#U|Q7xrv8UAI{C5#?%Kl>dkE=Fevbnk(+#5JNMC9x#cD!C{XNHG{07#Zmr80s1r yhZve$nOIsGS!x>?SQ!{Bf1M_Pq9HdwB{QuOw}xxm?yUi8VDNPHb6Mw<&;$TdJLVn$ literal 0 HcmV?d00001 diff --git a/backend/static/file-icons/standard/audio-document-svgrepo-com.svg b/backend/static/file-icons/standard/audio-document-svgrepo-com.svg new file mode 100644 index 0000000..09c3084 --- /dev/null +++ b/backend/static/file-icons/standard/audio-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/csv-document-svgrepo-com.svg b/backend/static/file-icons/standard/csv-document-svgrepo-com.svg new file mode 100644 index 0000000..d38147f --- /dev/null +++ b/backend/static/file-icons/standard/csv-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/excel-document-svgrepo-com.svg b/backend/static/file-icons/standard/excel-document-svgrepo-com.svg new file mode 100644 index 0000000..329e6bf --- /dev/null +++ b/backend/static/file-icons/standard/excel-document-svgrepo-com.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/exe-document-svgrepo-com.svg b/backend/static/file-icons/standard/exe-document-svgrepo-com.svg new file mode 100644 index 0000000..13325fa --- /dev/null +++ b/backend/static/file-icons/standard/exe-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/flash-document-svgrepo-com.svg b/backend/static/file-icons/standard/flash-document-svgrepo-com.svg new file mode 100644 index 0000000..678f126 --- /dev/null +++ b/backend/static/file-icons/standard/flash-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/html-document-svgrepo-com.svg b/backend/static/file-icons/standard/html-document-svgrepo-com.svg new file mode 100644 index 0000000..b7e91fb --- /dev/null +++ b/backend/static/file-icons/standard/html-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/image-document-svgrepo-com.svg b/backend/static/file-icons/standard/image-document-svgrepo-com.svg new file mode 100644 index 0000000..72b904c --- /dev/null +++ b/backend/static/file-icons/standard/image-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/mp4-document-svgrepo-com.svg b/backend/static/file-icons/standard/mp4-document-svgrepo-com.svg new file mode 100644 index 0000000..a6a9d51 --- /dev/null +++ b/backend/static/file-icons/standard/mp4-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/pages-document-svgrepo-com.svg b/backend/static/file-icons/standard/pages-document-svgrepo-com.svg new file mode 100644 index 0000000..739d626 --- /dev/null +++ b/backend/static/file-icons/standard/pages-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/pdf-document-svgrepo-com.svg b/backend/static/file-icons/standard/pdf-document-svgrepo-com.svg new file mode 100644 index 0000000..87e0779 --- /dev/null +++ b/backend/static/file-icons/standard/pdf-document-svgrepo-com.svg @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/psd-document-svgrepo-com.svg b/backend/static/file-icons/standard/psd-document-svgrepo-com.svg new file mode 100644 index 0000000..a69f6b0 --- /dev/null +++ b/backend/static/file-icons/standard/psd-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/rtf-document-svgrepo-com.svg b/backend/static/file-icons/standard/rtf-document-svgrepo-com.svg new file mode 100644 index 0000000..b7a2c08 --- /dev/null +++ b/backend/static/file-icons/standard/rtf-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/txt-document-svgrepo-com.svg b/backend/static/file-icons/standard/txt-document-svgrepo-com.svg new file mode 100644 index 0000000..b32a9f9 --- /dev/null +++ b/backend/static/file-icons/standard/txt-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/video-document-svgrepo-com.svg b/backend/static/file-icons/standard/video-document-svgrepo-com.svg new file mode 100644 index 0000000..2f3ef35 --- /dev/null +++ b/backend/static/file-icons/standard/video-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/visio-document-svgrepo-com.svg b/backend/static/file-icons/standard/visio-document-svgrepo-com.svg new file mode 100644 index 0000000..5eb1556 --- /dev/null +++ b/backend/static/file-icons/standard/visio-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/word-document-svgrepo-com.svg b/backend/static/file-icons/standard/word-document-svgrepo-com.svg new file mode 100644 index 0000000..4278239 --- /dev/null +++ b/backend/static/file-icons/standard/word-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/static/file-icons/standard/zip-document-svgrepo-com.svg b/backend/static/file-icons/standard/zip-document-svgrepo-com.svg new file mode 100644 index 0000000..73fc64c --- /dev/null +++ b/backend/static/file-icons/standard/zip-document-svgrepo-com.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/templates/pages/download.html b/backend/templates/pages/download.html index b54451e..189eb34 100644 --- a/backend/templates/pages/download.html +++ b/backend/templates/pages/download.html @@ -24,16 +24,25 @@ {{end}} {{if .Data.Files}} + {{$single := eq (len .Data.Files) 1}}
Expires {{.Data.ExpiresLabel}} {{if .Data.MaxDownloads}}{{.Data.DownloadCount}} / {{.Data.MaxDownloads}} downloads{{end}}
{{if not .Data.Locked}} - - - Download zip - + {{if $single}} + {{$first := index .Data.Files 0}} + + + Download + + {{else}} + + + Download zip + + {{end}} {{end}}