From 06b6c5b00c0b4676594f5f418de8225b4009c253 Mon Sep 17 00:00:00 2001 From: bakonpancakz Date: Sat, 23 May 2026 16:50:43 -0700 Subject: [PATCH] Initial Release --- .vscode/extensions.json | 6 + .vscode/settings.json | 26 +++ README | 21 +++ main.go | 314 +++++++++++++++++++++++++++++++++++ private/bg_graphic.png | Bin 0 -> 5206 bytes private/bg_pattern.png | Bin 0 -> 253 bytes private/directory.html | 73 ++++++++ private/favicon.png | Bin 0 -> 339 bytes private/icon_archive.png | Bin 0 -> 653 bytes private/icon_audio.png | Bin 0 -> 906 bytes private/icon_config.png | Bin 0 -> 486 bytes private/icon_document.png | Bin 0 -> 487 bytes private/icon_folder.png | Bin 0 -> 571 bytes private/icon_folder_open.png | Bin 0 -> 695 bytes private/icon_generic.png | Bin 0 -> 496 bytes private/icon_photo.png | Bin 0 -> 554 bytes private/icon_program.png | Bin 0 -> 389 bytes private/icon_redirect.png | Bin 0 -> 793 bytes private/icon_script.png | Bin 0 -> 510 bytes private/icon_video.png | Bin 0 -> 511 bytes private/styles.css | 109 ++++++++++++ public/.gitkeep | 4 + 22 files changed, 553 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 README create mode 100644 main.go create mode 100644 private/bg_graphic.png create mode 100644 private/bg_pattern.png create mode 100644 private/directory.html create mode 100644 private/favicon.png create mode 100644 private/icon_archive.png create mode 100644 private/icon_audio.png create mode 100644 private/icon_config.png create mode 100644 private/icon_document.png create mode 100644 private/icon_folder.png create mode 100644 private/icon_folder_open.png create mode 100644 private/icon_generic.png create mode 100644 private/icon_photo.png create mode 100644 private/icon_program.png create mode 100644 private/icon_redirect.png create mode 100644 private/icon_script.png create mode 100644 private/icon_video.png create mode 100644 private/styles.css create mode 100644 public/.gitkeep diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..becc7c8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "golang.go" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..22144ff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.rulers": [ + 120 + ], + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.associations": {}, + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/README b/README new file mode 100644 index 0000000..1c83db5 --- /dev/null +++ b/README @@ -0,0 +1,21 @@ +----------------- + pancakz-browser +----------------- + +A Miku flavored file browser inspired by THE FLAVOR FOLEY FILES (https://flavorfoley.com/FFF/) + + This application contains my personal branding, modify the files in the 'private' directory to change this. + +[ Config ] + +Configure the service using environment variables: + +| Name | Default | Description +| HTTP_ADDRESS | 127.0.0.1:9000 | Address and Port to use for requests +| HTTP_DIRECTORY | | Starting directory to list and serve files from + +[ Credits ] + +Original art that was modified for the background is available here: +https://www.pixiv.net/en/artworks/13891224 + diff --git a/main.go b/main.go new file mode 100644 index 0000000..ec4c058 --- /dev/null +++ b/main.go @@ -0,0 +1,314 @@ +package main + +import ( + "bytes" + "embed" + "fmt" + "io" + "log" + "mime" + "net/http" + "net/url" + "os" + "path" + "slices" + "strconv" + "strings" + "text/template" + "time" +) + +var ( + //go:embed private/* + privateDirectory embed.FS + serveHash = fmt.Sprintf("%x", time.Now().Unix()) + serveDirectory = os.Getenv("HTTP_DIRECTORY") + serveAddress = os.Getenv("HTTP_ADDRESS") + tmpl = template.Must( + template. + New("directory.html"). + Funcs(template.FuncMap{ + "EncodeURI": url.PathEscape, + "GetEntryIcon": getEntryIcon, + "GetEntrySizeHuman": getEntrySizeHuman, + }). + ParseFS(privateDirectory, "private/directory.html"), + ) +) + +type Entry struct { + Name string + IsDir bool + Size int64 + ModTime time.Time +} + +func serveFileError(w http.ResponseWriter, err error) { + if os.IsNotExist(err) { + w.WriteHeader(http.StatusNotFound) + return + } + if os.IsPermission(err) { + w.WriteHeader(http.StatusForbidden) + return + } + log.Println("Filesystem Error:", err) + http.Error(w, "Server Error", http.StatusInternalServerError) +} + +func parentPath(urlPath string) string { + trimmed := strings.TrimSuffix(urlPath, "/") + if trimmed == "" { + return "" + } + idx := strings.LastIndex(trimmed, "/") + if idx <= 0 { + return "/" + } + return trimmed[:idx+1] +} + +func getEntrySizeHuman(b int64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) +} + +func getEntryIcon(name string, isDir bool) string { + if isDir { + return "icon_folder.png" + } + switch strings.ToLower(path.Ext(name)) { + // Custom Filetypes + case ".redir": + return "icon_redirect.png" + + // Default Filetypes + case ".mp3", ".flac", ".ogg", ".wav", ".aac", ".opus", ".m4a": + return "icon_audio.png" + case ".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v": + return "icon_video.png" + case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif", ".bmp", ".tiff": + return "icon_photo.png" + case ".zip", ".tar", ".gz", ".7z", ".rar", ".zst", ".xz", ".bz2": + return "icon_archive.png" + case ".exe", ".elf", ".bin", ".out", ".appimage", ".deb", ".rpm": + return "icon_program.png" + case ".pdf", ".txt", ".md", ".srt", ".vtt", ".ass", ".log": + return "icon_document.png" + case ".html", ".ts", ".js", ".go", ".rs", ".pug": + return "icon_script.png" + case ".json", ".xml", ".yaml", ".toml", ".ini", ".cfg": + return "icon_config.png" + default: + return "icon_generic.png" + } +} + +func init() { + if serveAddress == "" { + serveAddress = "127.0.0.1:8080" + } + if serveDirectory == "" { + serveDirectory = "./public" + } +} + +func main() { + mux := http.NewServeMux() + prv := http.FileServerFS(privateDirectory) + + mux.HandleFunc("/private/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + prv.ServeHTTP(w, r) + }) + + mux.HandleFunc("/preferences/sort", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + http.SetCookie(w, &http.Cookie{ + Name: "sort", + Value: r.FormValue("sort"), + Path: "/", + MaxAge: 31536000, + }) + http.Redirect(w, r, r.FormValue("redirect"), http.StatusSeeOther) + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + + filepath := path.Join(serveDirectory, path.Clean(r.URL.Path)) + filestat, err := os.Stat(filepath) + if err != nil { + serveFileError(w, err) + return + } + + // Use Browser Cache (if possible) + modtime := filestat.ModTime().UTC() + modhash := fmt.Sprint(modtime.Unix()) + if r.Header.Get("If-None-Match") == modhash { + w.WriteHeader(http.StatusNotModified) + return + } + if ts, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil { + if !modtime.After(ts) { + w.WriteHeader(http.StatusNotModified) + return + } + } + if ts, err := time.Parse(http.TimeFormat, r.Header.Get("If-Unmodified-Since")); err == nil { + if modtime.After(ts) { + w.WriteHeader(http.StatusPreconditionFailed) + return + } + } + + if filestat.IsDir() { + // List Directory Contents + + indexPath := path.Join(filepath, "index.html") + if _, err := os.Stat(indexPath); err == nil { + http.ServeFile(w, r, indexPath) + return + } + + dirEntries, err := os.ReadDir(filepath) + if err != nil { + serveFileError(w, err) + return + } + + // Entry Information + entries := make([]Entry, 0, len(dirEntries)) + for _, entry := range dirEntries { + info, err := entry.Info() + if err != nil { + continue + } + + // Check Symbolic Link + isDir := entry.IsDir() + if entry.Type()&os.ModeSymlink != 0 { + if stat, err := os.Stat(path.Join(filepath, entry.Name())); err == nil { + isDir = stat.IsDir() + info = stat + } + } + + entries = append(entries, Entry{ + Name: entry.Name(), + IsDir: isDir, + Size: info.Size(), + ModTime: info.ModTime().UTC(), + }) + } + + // Entry Sorting + sortOrder := "alpha" + if c, err := r.Cookie("sort"); err == nil && c.Value != "" { + sortOrder = c.Value + } + slices.SortFunc(entries, func(a, b Entry) int { + // Folders are always on top + if a.IsDir != b.IsDir { + if a.IsDir { + return -1 + } + return 1 + } + // Use + if sortOrder == "modified" { + if a.ModTime.Equal(b.ModTime) { + return strings.Compare(a.Name, b.Name) + } + if a.ModTime.After(b.ModTime) { + return -1 + } + return 1 + } + return strings.Compare(a.Name, b.Name) + }) + + // Directory Listing + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, map[string]any{ + "Path": r.URL.Path, + "Parent": parentPath(r.URL.Path), + "Item": entries, + "ItemCount": len(entries), + "Version": serveHash, + "Sort": sortOrder, + }); err != nil { + serveFileError(w, err) + return + } + + } else { + // Serve File Contents + + f, err := os.Open(filepath) + if err != nil { + serveFileError(w, err) + return + } + defer f.Close() + + w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(filepath))) + w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat)) + w.Header().Set("ETag", modhash) + + // Special File Handler: .redir + if strings.EqualFold(path.Ext(filepath), ".redir") { + raw, _ := io.ReadAll(f) + uri, err := url.Parse(string(bytes.TrimSpace(raw))) + if err != nil { + serveFileError(w, err) + return + } + http.Redirect(w, r, uri.String(), http.StatusTemporaryRedirect) + return + } + + // Serve Byte Range (if applicable) + rangeHeader := r.Header.Get("Range") + if rangeHeader != "" { + var start, end int64 + fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end) + if end == 0 || end >= filestat.Size() { + end = filestat.Size() - 1 + } + length := end - start + 1 + w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, filestat.Size())) + w.Header().Set("Content-Length", strconv.FormatInt(length, 10)) + w.WriteHeader(http.StatusPartialContent) + f.Seek(start, io.SeekStart) + io.CopyN(w, f, length) + return + } + + // Serve Raw Contents + if _, err := io.Copy(w, f); err != nil { + serveFileError(w, err) + return + } + } + + }) + + svr := http.Server{ + Handler: mux, + Addr: serveAddress, + } + log.Printf("Listening @ %s\n", serveAddress) + if err := svr.ListenAndServe(); err != nil { + log.Fatalf("Listen Error: %s\n", err) + } +} diff --git a/private/bg_graphic.png b/private/bg_graphic.png new file mode 100644 index 0000000000000000000000000000000000000000..ea288dabd2c662939066aa3498f36544754ca7d9 GIT binary patch literal 5206 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 D*RW?S literal 0 HcmV?d00001 diff --git a/private/directory.html b/private/directory.html new file mode 100644 index 0000000..aef3112 --- /dev/null +++ b/private/directory.html @@ -0,0 +1,73 @@ + + + + + + + + + Browser: {{ .Path }} + + + + +
+
+ {{ .Path }} +
+ Links: + Top + Homepage + + Sort: +
+ + + +
+
+ + + +
+
+
+
+
+ + {{ if .Parent }} + +
+ ../ +
+ {{ end }} + + {{ range .Item }} + {{ if .IsDir }} + +
+ {{ .Name }} +
+ {{ else }} + + + {{ .Name }} + + {{ end }} + {{ end }} + +
+
+
+
+ + + diff --git a/private/favicon.png b/private/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3df34a029cc4123a0482d7309bfe1208af6757cb GIT binary patch literal 339 zcmV-Z0j&OsP)M4*POw*yG+U{{2N1qSv8wre3mCkhFuK!T^jB%m2qsH%kzOzW63`ijfbYyJ7)AA35k91hCHa!h4tX?SIWJ*AMovpm&IU~L(P!CCIlqBo ldL{@uCAj4mYm9Ndi{D@h6tB2peiHxy002ovPDHLkV1myxi?9Fy literal 0 HcmV?d00001 diff --git a/private/icon_archive.png b/private/icon_archive.png new file mode 100644 index 0000000000000000000000000000000000000000..729e8e2dec01015797c20830ebf5a97a801a027c GIT binary patch literal 653 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7e6l0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP$DY8C&U#UED7=pW^j0R11QeGnB?v5!k53-z#qtAFY)wsWq-xW#Hhi1 zr^Es1GzQfY*NBpo#FA92CkWnggVj!O-S zhTQy=%(P0N2183-0~1{X!w>^gD`R6TLkoz8q7RZ9Kn)sj8%i>BQ;SOya|_TcF##HH z4AHZ9#a>V-|Mzrp46!&pb#h|yAqAec&M7n2Zdqz|_tm!l|IJk|gfV@zxzE4%^wZ2w zY#oN43MoK7mW=u`9)y?0bS0($`+n7UvO~q3xO^!+wZ&hQAZf zj7!dQ?Tr)m=>=vr7c;F}^Ir6a=2oWDo9^VLG1rx!l@?%ktYGN>U%ueW0fzSJOcUg$ za9wgXY<}lBbJ3>@ejk^yr*>U`&ajl-()HBl*0}xMJ8KTz%3b!y!iRD9a(})WjyR?w zZGn@)tbf*?pI7tY#jdi(Pme5Yj4m8yS(1Nq`z@PVM&X$sKfKpbek&BTO7MEqcebPm qX67f&9bu0IEOVm2-1I*GP*#vp`R3=Jm5+fz!QkoY=d#Wzp$PzA?DHZ3 literal 0 HcmV?d00001 diff --git a/private/icon_audio.png b/private/icon_audio.png new file mode 100644 index 0000000000000000000000000000000000000000..82e9e8fd89291225e14efc8c7c6cad983d3f4075 GIT binary patch literal 906 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7e6l0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP~uO3Plzj!1_B2MhlU0QAX=~>VaXCtAj908;Q$Qu z_4Tb|3CphKffP8ems7B?D6BrMMXt2GBT@I7inv2&zUo4$&zfKIg1xBzIyfQ zjvYIWA3y%;)vI&oVzRTdKYskUdUfc>kI4-U4XanL?&~Z1|G#kOPVN8y`~Lqwwt96# zUmuWd`TzgpRjWd;UbVb>brl~UUuY;$OT*Qxg+G3@Z`!nJ=g!c_k3;X=$=2Exp&crj0FO|p)Cv%95aIdGi3Pwm3`sy zB9Kjh%QnT~#lc3#4?IgU*Ki*&=$v}Z;DNkY_mjqr7mw^%dU=~|?qieU0|w^XZzOb- zEfA5oz3!=_$KN%5-3iA_%5C{H<_EBBt*SlG>-1>3>8WGdGJ&)2+gsRhC9>s2&3Y(N z=6~t_yyc18nBCcSB}%Zg%ww{&c>O)L+(5yO{rS!24WAP%dKOuzMY*=Oc3;%!TfD+u zPgXM`Myh|kgo50q%gQR|JA2mU=RLOGwu!q_w$sKJSD)PhMhAnZtDnm{r-UW|bcl8A literal 0 HcmV?d00001 diff --git a/private/icon_config.png b/private/icon_config.png new file mode 100644 index 0000000000000000000000000000000000000000..7b46843ba31acd113ad63c0f2756ba83c6f30641 GIT binary patch literal 486 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBC?Oi)6XFU|(9rPz|9>Fkz<~n|4Gci$i5?)F zK#IZ0z{pJ3z*N`3D8#_f%Gk`x)L7fVz{NTVi2lVhGVQW#ad5Ks|FkT^vI+&M&>- z&DZ20z;Zz`&SBYJhPy|;MyS5wj5{D;$__WZ8noI`)}~C?!x|p!pcK^dYX3_ Y)BoCS%9;I`2k1}+Pgg&ebxsLQ0I)crbN~PV literal 0 HcmV?d00001 diff --git a/private/icon_document.png b/private/icon_document.png new file mode 100644 index 0000000000000000000000000000000000000000..d57f5d483f857d22ceb07539f6ef58fecd619204 GIT binary patch literal 487 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBC?OHx6XFU|(9rPz|9>Fkz<~n{3?L?uGb>+XB2b92 zB*-tA!Qt7BG!Q4r+uemJjk_iV$YC$>^mS!_#mdB}$T2HLDH158TH+c}l9E`GYL#4+ z3Zxi}42;Zl4NP?nj6w_ytPCuy42-ow41=c?=MJH0$jwj5OsfQHFtpS)Fwr$I3^6dZ zGB&m{w18+R`XH$R)Sv;kp(HamwYVfPw*bWwLy#T=sGi!z?jSw0JzX3_G|n&WJIKjo zDB!yJM#FBwJ^y*nm>P&mFo`LwblAWU>+xIdCX2D7|GvkM&!$hA_3T)nsG7+ajkIXj zRi$F;u}3$xMT?z2YIZQD<)W|6yVNN+xfo}}X80+o%=!_uG0i2yee&5?|pdV Tc#S%MS13By^p1!W^kJu!{ z8HG73<0k{9g*{yyLo808oqU$}kO5EYX^z~Zdy|+$#2hx&JU_rT?YlhVZ_7+!;lgJt zvs&Zr{=52I=0AJklj#087iv!*^IF&wBX_hSFIhpEeZoXxj}u>OUzvvAoLDNK^*TtD z(^&uJ2L(<~GaZ*BOF9KME^wHcP<`XGD)aNRuQ>I)`*}F}Tx1X4n{57IURevngDaxp z!e!CS`z<%9g|a%meIldF=slTR%cWB})I_uAqs!%WXM*+DEjcUGm+){!Z1;2*IgW$w9AeBGio=7pC9>E_ zt+bK(?Uq>Pp&h7aGRH_EZGnXEM30N_O69U<<|WtuZT%l3zrO47* Tn@_5M8W=oX{an^LB{Ts5_sh{q literal 0 HcmV?d00001 diff --git a/private/icon_folder_open.png b/private/icon_folder_open.png new file mode 100644 index 0000000000000000000000000000000000000000..5852574a757435f701e5405683dba1afd4ae2efd GIT binary patch literal 695 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)pT}UnMXwSj}Ky5HFasE6@fgVHw~P;tHhO+S-(ql>Yzz zaNxj!A3uIHG%)=CKl99)w3#y*o@M?NdG>=ji9gJH<@Ntw{?jF~{H3U2o!|P7 z4gaN;e&%;Rz4UayqL^3I*8ScU|4$WNyRVq_|68fI&er zxk80`ME=DZstPu?)o(Y|3HW6$ts}rz@c0;4*vIpqM4oc;?kG&Fl>Yz3HAID_!2gH~ z%bhO{M~)n1C}rJw@cRkVDuy>wEgVOrL>ZEwNGFOuS}xA16}Tghaf9jf58@MUZ)4~Z zH4Z5YdYLMbJwo--xB%4^}BnmMcuLX$aS5=KRbXWXiLtrs$UVxN9F{^wG+^-fvLRg11Le8~))m3C|PeEkPE z=g$m2P+)iIgZN*j$h&5V_mT{%fsvtF;u=wsl30>zm0Xkxq!^403@vmG%yf-RLX3>9 t42-Qzjdcx7tPBiJdf)X&(U6;;l9^VCTf+{q)X$*!_H^}gS?83{1OWcW3BCXT literal 0 HcmV?d00001 diff --git a/private/icon_generic.png b/private/icon_generic.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf0ebbafa2c113c6b72eec9524a998e94585857 GIT binary patch literal 496 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBD4`hO6XFU|(9rPz|9>Fkz<~oGssRXr03z1#TB)NM z$YU%C@(X5gcy=QV#7XjYcVSB7u1NuM*h@TpUD;o;GBIe1-zz^q87QP$;u=wsl30>z zm0Xkxq!^40jLdWmOmz*6LJSP73@oe+jI}`wgQpee4xwns%}>cps|0E=wA3{)(KRp( zF)+0B;Ln|YwC1-DPOabbd?djqeqH%ud z1w%e2MFG};bqijz%BU>7^QOOZ?L}z;zl2TVA2>|Dghf{evR^k~zI!a*ZpKOWADS~M}qHB&?p6TbR>05Eo(AA-_ z_~A+K)FVe5ws2I)?OgIy^+D|^sT0qqFg6CgSv!4N(j95vji2UaKCb`6+P?b<_XR`w Y>aA{?@7Fdh=ij|2`Q&E85z!oT^TH+c} zl9E`GYL#4+3Zxi}42;Zl4NP?nj6w_ytxOE9j19C646F<8+(mJ^_+25X?gIT?-PWkxbusKJ#uD{3_Df)t})b76d0zWG6{Mi90x@N zcI}>7mdeGk>UBm`w8Mm3PZGIHmL6oi9bMEUxI?eaynfxnd3MXY5+8V;mpPG?Fh9iN z!HpX!0u5rj^JSZayknG`%vqB6q|1poo^3L>I&(Tcu-Ego!?p$06XXgMd4ISs|HJ<* fT%22=<($1{di)lDmmAhV-!XW)`njxgN@xNA%7L_$ literal 0 HcmV?d00001 diff --git a/private/icon_program.png b/private/icon_program.png new file mode 100644 index 0000000000000000000000000000000000000000..4660f9ae2ec982f64660b873eb2d01cea254e7f9 GIT binary patch literal 389 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBC?OQ!6XFU|(9i%R4;(naz_9=S|NqC*8jb<^j3q&S z!3+-1Zlr-YN#5=*tUvN!g6v=~@$_|Nf5pngsK_xZMJW;}q*~${QIe8al4_M)lnSI6 zj0}v-bPY^(4U9qz46F<+tPG5`K@5YZ73U72Xvob^$xN#RYB03aH89aNFbpv;wK6ug zGPHnbDEc6&0o0%Yx1l66H?_DVF}DE45<`$41E`+b#qJhsn>&q2X+RNOWr>)1rkQ3Kby@8yKo)B)KdVk~+{)!|+un Vk6HiLL3f~044$rjF6*2UngE+HYn}iA literal 0 HcmV?d00001 diff --git a/private/icon_redirect.png b/private/icon_redirect.png new file mode 100644 index 0000000000000000000000000000000000000000..3370282058af8adad67a4479c4c977f16367289e GIT binary patch literal 793 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBD4`YL6XFV_8UFu2aNxlI{|pTc4L}U!g9wI({~#eC z4@iLc@6Agu04c_jAirP+hi5m^K%69RcNfl&H# zmbgZgq$HN4S|t~y0x1R~10yqC15;fC;}9cbDnC}Q!>*k zff@`gbq!2(4GcpJOs$NKtqd(78j3zhY5+B8z-=hW%uOvWNz5%kx5UiK$QYu>wB|t! z(9yd+T^vI+&bv^X?X55Z;t1ZmP_t-N)CJVj%_;Ru=2d_0=A`o zzq@qmFNO5|-j^2Wb^0xzri3!@gB>>=oTnb))XbWqyklAXrfs)$B!rjhFuu9f&NtU5 zSdmlth*0_av+A>A_#PF0JEd{xUfN%tC5vS*#tUdqM_3pZ}ZMM=( zmPrfPrM5;*%$#*lMY*f_$ZL07p4{I79G(J-mo!Tk|K{mT{&_cHl8X3UjZK^N8hQ40 znA8?5zExm0=cFTNq@dEy7Z1N(mQ60)XQaqddf0yI{HLGZ*Q{*(aOK@Lc{ACvDeGge zNqqb4UQ_>f@#mr$56!=7-@aqKzW>?usM>!AWS)noUW=63Tv2ssMtrsRztt60pUv-_ by<Fkz<~n{Kt@AD2ZzrxAfK@$ z$S;_|;n|He5GTpo-GwQQyCwz5VK4FYb!C6W%EYL_e5b?#XexthiEBhjN@7W>RdP`( zkYX@0Ff!9MFx53M3NbLSGO(~RFxCb!44zh;JA|SkH$NpatrDog&{EgHMAyJD#K6?b z*x1U@0-~YlgQNyfg9hA&lFZ!H;*!MN0u)OOtxSN%LoM06VlOCwwtBiahG?A6?LWwS zSb>M3)5>uH4@1*F;Wc~#2U#?PL$rSP2lzA<2EMPHlcRiOlhC4#H~Ma-Elm4(OYPwm zl^pMYEwYyw4ruypyD1kC|41`M^XPHYl0fF{ep^?e{-Apw=LNl3#P-j9ixcCGzzX@> zn%Bi*x9>R6a%SoY7QUZbo(L(wY?`E}p}4U2D67zq>Z$xf6*F(oUwpwLeiM_s+u5`S v9@{tTs)dFml{+-Ydi@daY<$Ef^O3be#6$EuyZKX~2N*nE{an^LB{Ts5w>+q& literal 0 HcmV?d00001 diff --git a/private/icon_video.png b/private/icon_video.png new file mode 100644 index 0000000000000000000000000000000000000000..34d2d4161d7c384832b7e587f375df7272cbc9fb GIT binary patch literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBC?OZ%6XFU|(9rPz|9>Fkz<~omx}kvq!~qfw3`gw? zfu=ApmIV0)GdMiEkp|)p1!W^uUMHF6**?5C`AH=R7+eVN>UO_ zQmvAUQh^kMk%5t!u7Rnpfl-Koft7)Um4UG~h+*)w;@lw=4Y~O#nQ4_k4ThGw1}3@& zh9L%~R>sCwh87SFMIR(JfEqO5Hk4%MrWThZ<`$q>VhGY>0M%2w*d3&2t*47)h{pNe zi@tn^6a-iw7*;VP?s3TEDGm4X4~VQ_T>pSkO8EP-?sc{s`Su5g*$GYh&KkwRz@YS$ zLow)R0`H>r4ob|kQZA=EI8WZnS&;E}S$0djygJ(z+2%{k8{Ro?@Y`UW{8H{+M_Zdu zwA@<(Hzw&_lEOz;oBm6Y`*LUR)%M1!m0u4z+FuP?`(hdIi=W%_!b9pk^Cu+cI~z4v pTh8ZcmpZlk5C2E$DGbwC_pQ0)+V(j)bp_Dj44$rjF6*2UngA+Gq`d$D literal 0 HcmV?d00001 diff --git a/private/styles.css b/private/styles.css new file mode 100644 index 0000000..9cb59b0 --- /dev/null +++ b/private/styles.css @@ -0,0 +1,109 @@ +body { + margin: 0; +} + +::-webkit-scrollbar { + display: none; +} + +a, +button { + border: 0; + padding: 0; + font-family: Times, serif; + background: transparent; + text-decoration: underline; + color: blue; + cursor: pointer; + font-size: 1em; +} + +div.foreground { + background: #88cecb; + box-sizing: border-box; + padding: 16px; + min-height: 180px; +} + +div.pattern { + background: url("/private/bg_pattern.png?v={{ .Version }}"); + background-repeat: repeat-x; + height: 152px; +} + +div.graphic { + background: url("/private/bg_graphic.png?v={{ .Version }}"); + width: 256px; + height: 259px; + position: absolute; + margin-top: 32px; + right: 2%; +} + +div.header { + display: flex; + justify-content: space-between; +} + +div.header div.actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +div.directory { + display: flex; + flex-wrap: wrap; +} + +a.entry[data-type="directory"] div.icon { + background-size: 32px 32px; + height: 32px; + width: 32px; +} + +a.entry { + display: inline-flex; + flex-direction: column; + align-items: center; + width: 100px; + padding: 8px; + text-align: center; + text-decoration: none; + color: inherit; +} + +a.entry img.icon { + height: 32px; + width: 32px; +} + +a.entry span.filename { + font-size: 0.8em; + margin-top: 4px; + color: blue; + text-decoration: underline; + word-break: break-word; +} + +@media (max-width: 600px) { + + a.entry { + padding: 4px; + width: 100%; + text-align: left; + flex-direction: unset; + gap: 8px; + } + + a.entry img.icon, + a.entry[data-type="directory"] div.icon { + background-size: 1em 1em; + height: 1em; + width: 1em; + } + + a.entry span.filename { + font-size: 1em; + } +} diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..7663cf6 --- /dev/null +++ b/public/.gitkeep @@ -0,0 +1,4 @@ +I KEPT A BULLET IN THE CHAMBER FOR A RAINY DAY +BUT WHAT'S THE POINT IN PLANNING FOR IT ANYWAY +I'VE GOT TOO MANY COLORS FOR A SHADE OF GREY +AND I CAN MAKE IT AUTUMN EVERY SINGLE DAY \ No newline at end of file