feat(config): support *_MB env vars for upload size limits
- Add `applyMegabytesOrBytesEnv` to accept size settings in either bytes or MB - Prefer `*_BYTES` when set, otherwise convert `*_MB` to bytes with overflow guard - Add coverage for MB-based environment overrides - Introduce `static/js/upload-popups.js` to lazy-load and cache popup templatesfeat(config): support *_MB env vars for upload size limits - Add `applyMegabytesOrBytesEnv` to accept size settings in either bytes or MB - Prefer `*_BYTES` when set, otherwise convert `*_MB` to bytes with overflow guard - Add coverage for MB-based environment overrides - Introduce `static/js/upload-popups.js` to lazy-load and cache popup templates
This commit is contained in:
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -197,10 +198,6 @@ func Load() (*Config, error) {
|
|||||||
}{
|
}{
|
||||||
{SettingDefaultGuestExpirySecs, "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", 0, &cfg.DefaultGuestExpirySeconds},
|
{SettingDefaultGuestExpirySecs, "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", 0, &cfg.DefaultGuestExpirySeconds},
|
||||||
{SettingMaxGuestExpirySecs, "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", 0, &cfg.MaxGuestExpirySeconds},
|
{SettingMaxGuestExpirySecs, "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", 0, &cfg.MaxGuestExpirySeconds},
|
||||||
{SettingGlobalMaxFileSizeBytes, "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", 0, &cfg.GlobalMaxFileSizeBytes},
|
|
||||||
{SettingGlobalMaxBoxSizeBytes, "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", 0, &cfg.GlobalMaxBoxSizeBytes},
|
|
||||||
{SettingDefaultUserMaxFileBytes, "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", 0, &cfg.DefaultUserMaxFileSizeBytes},
|
|
||||||
{SettingDefaultUserMaxBoxBytes, "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", 0, &cfg.DefaultUserMaxBoxSizeBytes},
|
|
||||||
{SettingSessionTTLSeconds, "WARPBOX_SESSION_TTL_SECONDS", 60, &cfg.SessionTTLSeconds},
|
{SettingSessionTTLSeconds, "WARPBOX_SESSION_TTL_SECONDS", 60, &cfg.SessionTTLSeconds},
|
||||||
}
|
}
|
||||||
for _, item := range envInt64s {
|
for _, item := range envInt64s {
|
||||||
@@ -208,6 +205,22 @@ func Load() (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sizeEnvVars := []struct {
|
||||||
|
key string
|
||||||
|
mbName string
|
||||||
|
bytesName string
|
||||||
|
target *int64
|
||||||
|
}{
|
||||||
|
{SettingGlobalMaxFileSizeBytes, "WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", &cfg.GlobalMaxFileSizeBytes},
|
||||||
|
{SettingGlobalMaxBoxSizeBytes, "WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", &cfg.GlobalMaxBoxSizeBytes},
|
||||||
|
{SettingDefaultUserMaxFileBytes, "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", &cfg.DefaultUserMaxFileSizeBytes},
|
||||||
|
{SettingDefaultUserMaxBoxBytes, "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", &cfg.DefaultUserMaxBoxSizeBytes},
|
||||||
|
}
|
||||||
|
for _, item := range sizeEnvVars {
|
||||||
|
if err := cfg.applyMegabytesOrBytesEnv(item.key, item.mbName, item.bytesName, 0, item.target); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
envInts := []struct {
|
envInts := []struct {
|
||||||
key string
|
key string
|
||||||
@@ -404,6 +417,34 @@ func (cfg *Config) applyInt64Env(key string, name string, min int64, target *int
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) applyMegabytesOrBytesEnv(key string, mbName string, bytesName string, min int64, target *int64) error {
|
||||||
|
if rawBytes := strings.TrimSpace(os.Getenv(bytesName)); rawBytes != "" {
|
||||||
|
parsed, err := parseInt64(rawBytes, min)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", bytesName, err)
|
||||||
|
}
|
||||||
|
*target = parsed
|
||||||
|
cfg.setValue(key, strconv.FormatInt(parsed, 10), SourceEnv)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMB := strings.TrimSpace(os.Getenv(mbName))
|
||||||
|
if rawMB == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsedMB, err := parseInt64(rawMB, min)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", mbName, err)
|
||||||
|
}
|
||||||
|
if parsedMB > math.MaxInt64/(1024*1024) {
|
||||||
|
return fmt.Errorf("%s: is too large", mbName)
|
||||||
|
}
|
||||||
|
parsedBytes := parsedMB * 1024 * 1024
|
||||||
|
*target = parsedBytes
|
||||||
|
cfg.setValue(key, strconv.FormatInt(parsedBytes, 10), SourceEnv)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *Config) applyIntEnv(key string, name string, min int, target *int) error {
|
func (cfg *Config) applyIntEnv(key string, name string, min int, target *int) error {
|
||||||
raw := strings.TrimSpace(os.Getenv(name))
|
raw := strings.TrimSpace(os.Getenv(name))
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
|
|||||||
@@ -64,6 +64,39 @@ func TestEnvironmentOverrides(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMegabyteSizeEnvironmentOverrides(t *testing.T) {
|
||||||
|
clearConfigEnv(t)
|
||||||
|
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "2048")
|
||||||
|
t.Setenv("WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "4096")
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.GlobalMaxFileSizeBytes != 2048*1024*1024 {
|
||||||
|
t.Fatalf("unexpected global max file size: %d", cfg.GlobalMaxFileSizeBytes)
|
||||||
|
}
|
||||||
|
if cfg.GlobalMaxBoxSizeBytes != 4096*1024*1024 {
|
||||||
|
t.Fatalf("unexpected global max box size: %d", cfg.GlobalMaxBoxSizeBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestByteSizeEnvironmentOverridesTakePrecedence(t *testing.T) {
|
||||||
|
clearConfigEnv(t)
|
||||||
|
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "2048")
|
||||||
|
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", "100")
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.GlobalMaxFileSizeBytes != 100 {
|
||||||
|
t.Fatalf("unexpected global max file size: %d", cfg.GlobalMaxFileSizeBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInvalidEnvironmentValues(t *testing.T) {
|
func TestInvalidEnvironmentValues(t *testing.T) {
|
||||||
clearConfigEnv(t)
|
clearConfigEnv(t)
|
||||||
t.Setenv("WARPBOX_SESSION_TTL_SECONDS", "1")
|
t.Setenv("WARPBOX_SESSION_TTL_SECONDS", "1")
|
||||||
@@ -131,9 +164,13 @@ func clearConfigEnv(t *testing.T) {
|
|||||||
"WARPBOX_RENEW_ON_DOWNLOAD_ENABLED",
|
"WARPBOX_RENEW_ON_DOWNLOAD_ENABLED",
|
||||||
"WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS",
|
"WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS",
|
||||||
"WARPBOX_MAX_GUEST_EXPIRY_SECONDS",
|
"WARPBOX_MAX_GUEST_EXPIRY_SECONDS",
|
||||||
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_MB",
|
||||||
"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES",
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES",
|
||||||
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_MB",
|
||||||
"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES",
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES",
|
||||||
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB",
|
||||||
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES",
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES",
|
||||||
|
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB",
|
||||||
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES",
|
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES",
|
||||||
"WARPBOX_SESSION_TTL_SECONDS",
|
"WARPBOX_SESSION_TTL_SECONDS",
|
||||||
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
||||||
|
|||||||
29
run.sh
Executable file
29
run.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Core service switches.
|
||||||
|
export WARPBOX_GUEST_UPLOADS_ENABLED="${WARPBOX_GUEST_UPLOADS_ENABLED:-true}"
|
||||||
|
export WARPBOX_API_ENABLED="${WARPBOX_API_ENABLED:-true}"
|
||||||
|
export WARPBOX_ZIP_DOWNLOADS_ENABLED="${WARPBOX_ZIP_DOWNLOADS_ENABLED:-true}"
|
||||||
|
export WARPBOX_ONE_TIME_DOWNLOADS_ENABLED="${WARPBOX_ONE_TIME_DOWNLOADS_ENABLED:-true}"
|
||||||
|
|
||||||
|
# Storage and expiry limits used by the upload UI and backend validators.
|
||||||
|
# Use megabytes here; WarpBox converts these to bytes internally.
|
||||||
|
export WARPBOX_GLOBAL_MAX_FILE_SIZE_MB="${WARPBOX_GLOBAL_MAX_FILE_SIZE_MB:-2048}" # 2 GiB
|
||||||
|
export WARPBOX_GLOBAL_MAX_BOX_SIZE_MB="${WARPBOX_GLOBAL_MAX_BOX_SIZE_MB:-4096}" # 4 GiB
|
||||||
|
export WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS="${WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS:-3600}" # 1 hour
|
||||||
|
export WARPBOX_MAX_GUEST_EXPIRY_SECONDS="${WARPBOX_MAX_GUEST_EXPIRY_SECONDS:-172800}" # 48 hours
|
||||||
|
|
||||||
|
# Download-page refresh and thumbnail worker tuning.
|
||||||
|
export WARPBOX_BOX_POLL_INTERVAL_MS="${WARPBOX_BOX_POLL_INTERVAL_MS:-5000}"
|
||||||
|
export WARPBOX_THUMBNAIL_BATCH_SIZE="${WARPBOX_THUMBNAIL_BATCH_SIZE:-10}"
|
||||||
|
export WARPBOX_THUMBNAIL_INTERVAL_SECONDS="${WARPBOX_THUMBNAIL_INTERVAL_SECONDS:-30}"
|
||||||
|
|
||||||
|
# Data location.
|
||||||
|
export WARPBOX_DATA_DIR="${WARPBOX_DATA_DIR:-./data}"
|
||||||
|
|
||||||
|
# Admin Area
|
||||||
|
export WARPBOX_ADMIN_ENABLED="${WARPBOX_ADMIN_ENABLED:-true}"
|
||||||
|
export WARPBOX_ADMIN_PASSWORD="${WARPBOX_ADMIN_PASSWORD:-123}"
|
||||||
|
|
||||||
|
go run ./cmd/main.go run
|
||||||
@@ -22,10 +22,13 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: 'PixelOperator', 'MS Sans Serif', Arial, sans-serif;
|
font-family: 'PixelOperator', 'MS Sans Serif', Arial, sans-serif;
|
||||||
font-smooth: never;
|
font-smooth: never;
|
||||||
|
-webkit-font-smoothing: none;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
cursor: url('/static/cursors/vaporwave-hotline-white-plus/Normal\ Select.cur'), auto;
|
|
||||||
|
|
||||||
--base-font-size: 14px;
|
--base-font-size: 14px;
|
||||||
|
--ui-scale: 1;
|
||||||
--w98-blue: #000078;
|
--w98-blue: #000078;
|
||||||
--w98-blue-gradient: linear-gradient(90deg, #000078 0%, #000078 28%, #0f80cd 50%, #000078 72%, #000078 100%);
|
--w98-blue-gradient: linear-gradient(90deg, #000078 0%, #000078 28%, #0f80cd 50%, #000078 72%, #000078 100%);
|
||||||
--w98-gray: #c0c0c0;
|
--w98-gray: #c0c0c0;
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
scrollbar-width: auto;
|
scrollbar-width: auto;
|
||||||
scrollbar-color: #c0c0c0 #808080;
|
scrollbar-color: #c0c0c0 #808080;
|
||||||
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@@ -75,7 +79,7 @@ label[for],
|
|||||||
.menu-button,
|
.menu-button,
|
||||||
.win98-button:not(:disabled),
|
.win98-button:not(:disabled),
|
||||||
a {
|
a {
|
||||||
cursor: url('/static/cursors/vaporwave-hotline-white-plus/Link\ Select.cur'), pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
@@ -90,7 +94,7 @@ input[type="password"],
|
|||||||
input[type="number"],
|
input[type="number"],
|
||||||
input[type="file"],
|
input[type="file"],
|
||||||
textarea {
|
textarea {
|
||||||
cursor: url('/static/cursors/vaporwave-hotline-white-plus/Hotline\ Black\ Handwriting.cur'), text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
@@ -172,18 +176,19 @@ textarea:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1800px) {
|
@media (min-width: 1800px) {
|
||||||
:root { --base-font-size: 15px; }
|
:root { --base-font-size: 15px; --ui-scale: 1.2; }
|
||||||
.desktop-wrap { zoom: 1.2; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 2048px) {
|
@media (min-width: 2048px) {
|
||||||
:root { --base-font-size: 16px; }
|
:root { --base-font-size: 16px; --ui-scale: 1.36; }
|
||||||
.desktop-wrap { zoom: 1.36; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 2560px) {
|
@media (min-width: 2560px) {
|
||||||
:root { --base-font-size: 18px; }
|
:root { --base-font-size: 18px; --ui-scale: 1.58; }
|
||||||
.desktop-wrap { zoom: 1.58; }
|
}
|
||||||
|
|
||||||
|
@media (min-width: 3200px) {
|
||||||
|
:root { --base-font-size: 20px; --ui-scale: 1.88; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.box-window {
|
.box-window {
|
||||||
width: min(760px, calc(100vw - 36px));
|
width: min(760px, calc(100vw - 36px));
|
||||||
height: min(560px, calc(100vh - 36px));
|
height: min(560px, calc(100vh - 36px));
|
||||||
|
zoom: var(--ui-scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-toolbar {
|
.box-toolbar {
|
||||||
@@ -192,6 +193,7 @@
|
|||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
zoom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-titlebar {
|
.box-titlebar {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.login-window {
|
.login-window {
|
||||||
width: 420px;
|
width: 420px;
|
||||||
height: 248px;
|
height: 248px;
|
||||||
|
zoom: var(--ui-scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
@@ -109,6 +110,7 @@
|
|||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
zoom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-titlebar {
|
.login-titlebar {
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
zoom: var(--ui-scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.fit-window .desktop-wrap {
|
||||||
|
width: min(100%, calc(100vw / var(--ui-scale) - 20px));
|
||||||
|
height: min(calc(100vh / var(--ui-scale) - 20px), 900px);
|
||||||
|
max-height: none;
|
||||||
|
grid-template-columns: minmax(0, 1fr) var(--side-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-window {
|
.upload-window {
|
||||||
@@ -233,6 +241,7 @@
|
|||||||
background-color: #000078;
|
background-color: #000078;
|
||||||
background-image: repeating-linear-gradient(to right, rgba(255,255,255,.12) 0 1px, transparent 1px 18px);
|
background-image: repeating-linear-gradient(to right, rgba(255,255,255,.12) 0 1px, transparent 1px 18px);
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-quota-bar.is-over-quota {
|
.upload-quota-bar.is-over-quota {
|
||||||
@@ -433,6 +442,26 @@
|
|||||||
.upload-file-row.is-uploaded .upload-progress-bar { background-color: #008000; }
|
.upload-file-row.is-uploaded .upload-progress-bar { background-color: #008000; }
|
||||||
.upload-file-row.is-failed .upload-progress-bar { width: 100%; background-color: #800000; }
|
.upload-file-row.is-failed .upload-progress-bar { width: 100%; background-color: #800000; }
|
||||||
|
|
||||||
|
.upload-progress-bar.just-completed,
|
||||||
|
.upload-overall-bar.just-completed {
|
||||||
|
animation: progress-impact-bar 520ms steps(5, end) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress-bar.just-completed::after,
|
||||||
|
.upload-overall-bar.just-completed::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -7px;
|
||||||
|
top: 50%;
|
||||||
|
width: 12px;
|
||||||
|
height: 22px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: repeating-linear-gradient(45deg, rgba(255,255,255,.95) 0 2px, rgba(0,255,102,.85) 2px 4px, transparent 4px 6px);
|
||||||
|
box-shadow: 0 0 0 1px #ffffff, 0 0 8px #00ff66;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: progress-impact-spark 520ms steps(5, end) 1;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-result {
|
.upload-result {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 72px minmax(0, 1fr) 72px;
|
grid-template-columns: 72px minmax(0, 1fr) 72px;
|
||||||
@@ -828,6 +857,7 @@
|
|||||||
max-height: min(760px, calc(100vh - 24px));
|
max-height: min(760px, calc(100vh - 24px));
|
||||||
display: none;
|
display: none;
|
||||||
z-index: 80;
|
z-index: 80;
|
||||||
|
zoom: var(--ui-scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-window.is-visible {
|
.popup-window.is-visible {
|
||||||
@@ -888,6 +918,7 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
box-shadow: 4px 4px 0 rgba(0,0,0,.45);
|
box-shadow: 4px 4px 0 rgba(0,0,0,.45);
|
||||||
|
zoom: var(--ui-scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.is-visible {
|
.toast.is-visible {
|
||||||
@@ -984,6 +1015,17 @@
|
|||||||
font-family: 'PixelOperatorMono', monospace;
|
font-family: 'PixelOperatorMono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-body pre {
|
||||||
|
user-select: text;
|
||||||
|
cursor: text;
|
||||||
|
padding-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body pre::after {
|
||||||
|
content: "\A";
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
.kbd {
|
.kbd {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 18px;
|
min-width: 18px;
|
||||||
@@ -1002,6 +1044,8 @@
|
|||||||
@keyframes share-ready-pulse { 50% { filter: brightness(1.08); box-shadow: 0 0 0 2px #000078; } }
|
@keyframes share-ready-pulse { 50% { filter: brightness(1.08); box-shadow: 0 0 0 2px #000078; } }
|
||||||
@keyframes start-ready-rainbow-breathe { 0%, 100% { transform: rotate(-.35deg) scale(1); } 50% { transform: rotate(.35deg) scale(1.016); } }
|
@keyframes start-ready-rainbow-breathe { 0%, 100% { transform: rotate(-.35deg) scale(1); } 50% { transform: rotate(.35deg) scale(1.016); } }
|
||||||
@keyframes start-border-rainbow-slide { from { background-position: 0% 50%; } to { background-position: 100% 50%; } }
|
@keyframes start-border-rainbow-slide { from { background-position: 0% 50%; } to { background-position: 100% 50%; } }
|
||||||
|
@keyframes progress-impact-bar { 0% { filter: brightness(1); } 35% { filter: brightness(1.75); } 100% { filter: brightness(1); } }
|
||||||
|
@keyframes progress-impact-spark { 0% { opacity: 0; transform: translateY(-50%) scale(.7); } 30% { opacity: 1; transform: translateY(-50%) scale(1.18); } 100% { opacity: 0; transform: translateY(-50%) scale(.7); } }
|
||||||
@keyframes terminal-cursor { 50% { opacity: 0; } }
|
@keyframes terminal-cursor { 50% { opacity: 0; } }
|
||||||
@keyframes popup-open-v10 { from { transform: translate(-50%, -48%) scale(.97); opacity: .35; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } }
|
@keyframes popup-open-v10 { from { transform: translate(-50%, -48%) scale(.97); opacity: .35; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } }
|
||||||
@keyframes toast-in { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
@keyframes toast-in { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||||
|
|||||||
269
static/js/app.js
269
static/js/app.js
@@ -54,6 +54,8 @@ let uploadLocked = false;
|
|||||||
let statusTimer = null;
|
let statusTimer = null;
|
||||||
let pendingDuplicateFiles = [];
|
let pendingDuplicateFiles = [];
|
||||||
let apiKeyTimer = null;
|
let apiKeyTimer = null;
|
||||||
|
let completedImpactKeys = new Set();
|
||||||
|
let overallImpactDone = false;
|
||||||
|
|
||||||
function numberFromDataset(value) {
|
function numberFromDataset(value) {
|
||||||
const number = Number.parseInt(value || "0", 10);
|
const number = Number.parseInt(value || "0", 10);
|
||||||
@@ -177,6 +179,28 @@ function showToast(message, type = "info") {
|
|||||||
showToast.timer = setTimeout(() => el.toast.classList.remove("is-visible"), 2600);
|
showToast.timer = setTimeout(() => el.toast.classList.remove("is-visible"), 2600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disabledReasonFor(target) {
|
||||||
|
const control = target.closest("[data-disabled-reason], button, input, select, textarea, .upload-dropzone");
|
||||||
|
if (!control) return "";
|
||||||
|
if (control.classList.contains("upload-dropzone") && uploadLocked) {
|
||||||
|
return control.dataset.disabledReason || "The current box is sealed after upload. Press Clear to start a new box.";
|
||||||
|
}
|
||||||
|
if (control.disabled || control.readOnly || control.getAttribute("aria-disabled") === "true") {
|
||||||
|
return control.dataset.disabledReason || control.title || "This control is disabled right now.";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function announceDisabledReason(event) {
|
||||||
|
const reason = disabledReasonFor(event.target);
|
||||||
|
if (!reason) return false;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
showToast(reason, "warning");
|
||||||
|
setStatus(reason);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function stopStatusAnimation() {
|
function stopStatusAnimation() {
|
||||||
if (statusTimer) {
|
if (statusTimer) {
|
||||||
clearInterval(statusTimer);
|
clearInterval(statusTimer);
|
||||||
@@ -214,6 +238,14 @@ function setOverallProgress(percent) {
|
|||||||
if (el.overallPercent) el.overallPercent.textContent = display;
|
if (el.overallPercent) el.overallPercent.textContent = display;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flashProgressBar(bar) {
|
||||||
|
if (!bar) return;
|
||||||
|
bar.classList.remove("just-completed");
|
||||||
|
void bar.offsetWidth;
|
||||||
|
bar.classList.add("just-completed");
|
||||||
|
setTimeout(() => bar.classList.remove("just-completed"), 620);
|
||||||
|
}
|
||||||
|
|
||||||
function setRowProgress(item, percent) {
|
function setRowProgress(item, percent) {
|
||||||
const bar = item.row?.querySelector(".upload-progress-bar");
|
const bar = item.row?.querySelector(".upload-progress-bar");
|
||||||
if (bar) bar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
if (bar) bar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
||||||
@@ -275,6 +307,10 @@ function updateOverallProgress() {
|
|||||||
const uploadedCount = files.filter((item) => item.uploaded).length;
|
const uploadedCount = files.filter((item) => item.uploaded).length;
|
||||||
const percent = overallProgress();
|
const percent = overallProgress();
|
||||||
setOverallProgress(percent >= 100 && uploadedCount < files.length ? 99 : percent);
|
setOverallProgress(percent >= 100 && uploadedCount < files.length ? 99 : percent);
|
||||||
|
if (percent >= 100 && files.length && !overallImpactDone) {
|
||||||
|
overallImpactDone = true;
|
||||||
|
flashProgressBar(el.overallBar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFileRow(item, index) {
|
function createFileRow(item, index) {
|
||||||
@@ -310,6 +346,7 @@ function createFileRow(item, index) {
|
|||||||
remove.dataset.remove = String(index);
|
remove.dataset.remove = String(index);
|
||||||
remove.title = uploadLocked ? "This file cannot be removed because this upload box was already created." : "Remove file";
|
remove.title = uploadLocked ? "This file cannot be removed because this upload box was already created." : "Remove file";
|
||||||
remove.disabled = uploadLocked;
|
remove.disabled = uploadLocked;
|
||||||
|
remove.dataset.disabledReason = uploadLocked ? "Files cannot be removed after the box is created. Press Clear to start another upload." : "";
|
||||||
|
|
||||||
const progress = document.createElement("span");
|
const progress = document.createElement("span");
|
||||||
progress.className = "upload-progress";
|
progress.className = "upload-progress";
|
||||||
@@ -395,17 +432,9 @@ function addFiles(fileList) {
|
|||||||
function showDuplicateDialog(duplicates) {
|
function showDuplicateDialog(duplicates) {
|
||||||
pendingDuplicateFiles = duplicates;
|
pendingDuplicateFiles = duplicates;
|
||||||
const list = duplicates.map((item) => `<li><strong>${htmlEscape(item.displayName)}</strong> <span>${formatBytes(item.file.size)}</span></li>`).join("");
|
const list = duplicates.map((item) => `<li><strong>${htmlEscape(item.displayName)}</strong> <span>${formatBytes(item.file.size)}</span></li>`).join("");
|
||||||
openPopup("Duplicate file names", `
|
showTemplatePopup("Duplicate file names", "duplicate", { list })
|
||||||
<h3>Duplicate file names detected</h3>
|
.then(() => document.querySelector("#duplicate-append")?.focus());
|
||||||
<p>These files have the same names as files already in the queue.</p>
|
|
||||||
<ol class="duplicate-list">${list}</ol>
|
|
||||||
<p>Skip them, or append numbers so they become names like <code>file (2).zip</code>.</p>
|
|
||||||
<div class="copy-fallback-actions">
|
|
||||||
<button class="win98-button" type="button" id="duplicate-append">Append numbers</button>
|
|
||||||
<button class="win98-button" type="button" id="duplicate-skip">Skip duplicates</button>
|
|
||||||
</div>`);
|
|
||||||
showToast("Duplicate names found. Choose skip or append numbers.", "warning");
|
showToast("Duplicate names found. Choose skip or append numbers.", "warning");
|
||||||
setTimeout(() => document.querySelector("#duplicate-append")?.focus(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendPendingDuplicates() {
|
function appendPendingDuplicates() {
|
||||||
@@ -443,6 +472,8 @@ function clearQueue() {
|
|||||||
files = [];
|
files = [];
|
||||||
pendingDuplicateFiles = [];
|
pendingDuplicateFiles = [];
|
||||||
uploadLocked = false;
|
uploadLocked = false;
|
||||||
|
completedImpactKeys = new Set();
|
||||||
|
overallImpactDone = false;
|
||||||
stopStatusAnimation();
|
stopStatusAnimation();
|
||||||
setBoxOptionsLocked(false);
|
setBoxOptionsLocked(false);
|
||||||
setShareUrl("");
|
setShareUrl("");
|
||||||
@@ -461,14 +492,8 @@ function confirmClearQueue() {
|
|||||||
showToast("Nothing to clear.");
|
showToast("Nothing to clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openPopup("Clear WarpBox?", `
|
showTemplatePopup("Clear WarpBox?", "clear")
|
||||||
<h3>Confirm clear</h3>
|
.then(() => document.querySelector("#confirm-clear-no")?.focus());
|
||||||
<p>This removes the current queue, resets progress, and unlocks the Start upload button.</p>
|
|
||||||
<div class="copy-fallback-actions">
|
|
||||||
<button class="win98-button" type="button" id="confirm-clear-yes">Clear</button>
|
|
||||||
<button class="win98-button" type="button" id="confirm-clear-no">Cancel</button>
|
|
||||||
</div>`);
|
|
||||||
setTimeout(() => document.querySelector("#confirm-clear-no")?.focus(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBox() {
|
async function createBox() {
|
||||||
@@ -521,6 +546,13 @@ function setFileFailed(item, message) {
|
|||||||
updateOverallProgress();
|
updateOverallProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markCompletedImpact(item) {
|
||||||
|
const key = item.boxFile?.id || item.displayName;
|
||||||
|
if (completedImpactKeys.has(key)) return;
|
||||||
|
completedImpactKeys.add(key);
|
||||||
|
flashProgressBar(item.row?.querySelector(".upload-progress-bar"));
|
||||||
|
}
|
||||||
|
|
||||||
function uploadFile(item, onComplete) {
|
function uploadFile(item, onComplete) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
@@ -566,6 +598,7 @@ function uploadFile(item, onComplete) {
|
|||||||
item.row?.classList.add("is-uploaded");
|
item.row?.classList.add("is-uploaded");
|
||||||
if (item.row) item.row.title = "Uploaded";
|
if (item.row) item.row.title = "Uploaded";
|
||||||
setRowProgress(item, 100);
|
setRowProgress(item, 100);
|
||||||
|
markCompletedImpact(item);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(xhr.responseText);
|
const result = JSON.parse(xhr.responseText);
|
||||||
@@ -635,6 +668,8 @@ async function startUpload() {
|
|||||||
item.failed = false;
|
item.failed = false;
|
||||||
item.error = "";
|
item.error = "";
|
||||||
});
|
});
|
||||||
|
completedImpactKeys = new Set();
|
||||||
|
overallImpactDone = false;
|
||||||
renderFiles();
|
renderFiles();
|
||||||
|
|
||||||
let completedCount = 0;
|
let completedCount = 0;
|
||||||
@@ -731,6 +766,12 @@ function updateDisabledReasons() {
|
|||||||
el.startButton.dataset.disabledReason = reason;
|
el.startButton.dataset.disabledReason = reason;
|
||||||
el.startButton.title = reason;
|
el.startButton.title = reason;
|
||||||
}
|
}
|
||||||
|
if (el.fileInput) {
|
||||||
|
el.fileInput.dataset.disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : "");
|
||||||
|
}
|
||||||
|
if (el.dropzone) {
|
||||||
|
el.dropzone.dataset.disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
@@ -773,9 +814,6 @@ function loadSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncMenuChecks() {
|
function syncMenuChecks() {
|
||||||
document.querySelectorAll("[data-expiry-check]").forEach((node) => {
|
|
||||||
node.textContent = node.dataset.expiryCheck === el.expiry?.value ? "✓" : "";
|
|
||||||
});
|
|
||||||
const downloadCheck = document.querySelector("[data-download-page-check]");
|
const downloadCheck = document.querySelector("[data-download-page-check]");
|
||||||
if (downloadCheck) downloadCheck.textContent = el.downloadPage?.checked ? "✓" : "";
|
if (downloadCheck) downloadCheck.textContent = el.downloadPage?.checked ? "✓" : "";
|
||||||
}
|
}
|
||||||
@@ -826,6 +864,14 @@ function slugify(value) {
|
|||||||
.slice(0, 32);
|
.slice(0, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeSlugInput(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]/g, "")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.slice(0, 32);
|
||||||
|
}
|
||||||
|
|
||||||
function syncSlugFromName(force = false) {
|
function syncSlugFromName(force = false) {
|
||||||
if (!el.customSlug || !el.boxName) return;
|
if (!el.customSlug || !el.boxName) return;
|
||||||
if (force || !el.customSlug.value || el.customSlug.dataset.auto === "true") {
|
if (force || !el.customSlug.value || el.customSlug.dataset.auto === "true") {
|
||||||
@@ -846,9 +892,9 @@ function randomPassword() {
|
|||||||
|
|
||||||
function randomBoxName() {
|
function randomBoxName() {
|
||||||
if (!el.boxName || uploadLocked) return;
|
if (!el.boxName || uploadLocked) return;
|
||||||
const adjectives = ["neon", "turbo", "quiet", "cosmic", "lucky", "midnight", "pixel", "rapid"];
|
const adjectives = ["Neon", "Turbo", "Quiet", "Cosmic", "Lucky", "Midnight", "Pixel", "Rapid"];
|
||||||
const nouns = ["floppy", "archive", "packet", "portal", "folder", "upload", "cache", "drive"];
|
const nouns = ["Floppy Disk", "Archive Box", "Packet Portal", "Upload Folder", "Cache Drive", "Release Bundle"];
|
||||||
el.boxName.value = `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}`;
|
el.boxName.value = `${adjectives[Math.floor(Math.random() * adjectives.length)]} ${nouns[Math.floor(Math.random() * nouns.length)]}`;
|
||||||
syncSlugFromName(true);
|
syncSlugFromName(true);
|
||||||
setStatus("Generated a local box name");
|
setStatus("Generated a local box name");
|
||||||
}
|
}
|
||||||
@@ -891,14 +937,11 @@ async function copyText(kind, value, openUrl = "") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showCopyFallback(kind, value, openUrl) {
|
function showCopyFallback(kind, value, openUrl) {
|
||||||
openPopup(`${kind} copy failed`, `
|
const openLink = openUrl ? `<a class="win98-button" href="${htmlEscape(openUrl)}" target="_blank" rel="noreferrer">Open</a>` : "";
|
||||||
<h3>Clipboard access failed</h3>
|
showTemplatePopup(`${kind} copy failed`, "copy-failed", {
|
||||||
<p>The browser refused clipboard access. Copy it manually from the field below.</p>
|
value: htmlEscape(value),
|
||||||
<textarea class="copy-fallback-text" readonly>${htmlEscape(value)}</textarea>
|
openLink,
|
||||||
<div class="copy-fallback-actions">
|
});
|
||||||
${openUrl ? `<a class="win98-button" href="${htmlEscape(openUrl)}" target="_blank" rel="noreferrer">Open</a>` : ""}
|
|
||||||
<button class="win98-button" type="button" id="fallback-close">Close</button>
|
|
||||||
</div>`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function quotaWarningHtml(message) {
|
function quotaWarningHtml(message) {
|
||||||
@@ -916,10 +959,10 @@ function quotaWarningHtml(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showWarningDialog(title, message) {
|
function showWarningDialog(title, message) {
|
||||||
openPopup(title, `
|
showTemplatePopup(title, "warning", {
|
||||||
<h3>${htmlEscape(title)}</h3>
|
title: htmlEscape(title),
|
||||||
${quotaWarningHtml(message)}
|
content: quotaWarningHtml(message),
|
||||||
<div class="copy-fallback-actions"><button class="win98-button" type="button" id="fallback-close">OK</button></div>`);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPopup(title, html, about = false) {
|
function openPopup(title, html, about = false) {
|
||||||
@@ -936,96 +979,41 @@ function closeDoc() {
|
|||||||
el.modalBackdrop?.classList.remove("is-visible");
|
el.modalBackdrop?.classList.remove("is-visible");
|
||||||
}
|
}
|
||||||
|
|
||||||
const docs = {
|
async function showTemplatePopup(title, templateName, data = {}, about = false) {
|
||||||
cli: {
|
try {
|
||||||
title: "CLI Guide",
|
const html = await window.WBPopups.renderTemplate(templateName, data);
|
||||||
html: `
|
openPopup(title, html, about);
|
||||||
<h3>Upload with cURL</h3>
|
} catch (error) {
|
||||||
<p>WarpBox accepts normal multipart form uploads through the compatibility endpoint:</p>
|
showToast(error.message || `Could not load ${title}.`, "error");
|
||||||
<pre>curl \\
|
}
|
||||||
-F 'files=@./my-file.zip' \\
|
}
|
||||||
-F 'retention=1h' \\
|
|
||||||
${window.location.origin}/upload</pre>
|
|
||||||
<h4>Browser flow</h4>
|
|
||||||
<p>The browser uses the manifest API: it creates a box, uploads each file, and marks failed uploads so the download page does not wait forever.</p>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
faq: {
|
|
||||||
title: "Help & FAQ",
|
|
||||||
html: `
|
|
||||||
<h3>Help & FAQ</h3>
|
|
||||||
<section class="shortcut-section">
|
|
||||||
<h4>Keyboard shortcuts</h4>
|
|
||||||
<ul class="shortcut-list">
|
|
||||||
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">O</span></span><span>Browse for files.</span></li>
|
|
||||||
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">U</span></span><span>Start the current upload.</span></li>
|
|
||||||
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">K</span></span><span>Copy the full cURL command.</span></li>
|
|
||||||
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">L</span></span><span>Copy the share URL after upload.</span></li>
|
|
||||||
<li><span><span class="kbd">F1</span></span><span>Open this window.</span></li>
|
|
||||||
<li><span><span class="kbd">Esc</span></span><span>Close menus and popups.</span></li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<div class="faq-list">
|
|
||||||
<div class="faq-item"><p><strong>Can I password protect uploads?</strong></p><p>Yes. Set a password in Box Options before starting the upload.</p></div>
|
|
||||||
<div class="faq-item"><p><strong>What happens if one file fails?</strong></p><p>The failed row stays red, successful files remain available, and WarpBox marks the failed file in the manifest.</p></div>
|
|
||||||
<div class="faq-item"><p><strong>Are all options server-backed?</strong></p><p>Expiry, password, ZIP download, and one-time download are sent to the backend. Notes like box name, custom slug, and API key mode are saved locally until backend support exists.</p></div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
dailyQuota: {
|
|
||||||
title: "Upload limits",
|
|
||||||
html: `
|
|
||||||
<h3>Upload limits</h3>
|
|
||||||
<div class="quota-meter-list">
|
|
||||||
<div class="quota-meter">
|
|
||||||
<div class="quota-meter-head"><span>Box size</span><span>${maxBoxBytes ? formatBytes(maxBoxBytes) : "No configured limit"}</span></div>
|
|
||||||
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:${maxBoxBytes ? Math.min(100, Math.round((totalBytes() / maxBoxBytes) * 100)) : 0}%"></span></div>
|
|
||||||
</div>
|
|
||||||
<div class="quota-meter">
|
|
||||||
<div class="quota-meter-head"><span>Single file</span><span>${maxFileBytes ? formatBytes(maxFileBytes) : "No configured limit"}</span></div>
|
|
||||||
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:${oversizedFiles().length ? 100 : 0}%"></span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="quota-note">These values come from the running WarpBox configuration.</p>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
about: {
|
|
||||||
title: "About WarpBox",
|
|
||||||
about: true,
|
|
||||||
html: `
|
|
||||||
<h3>WarpBox</h3>
|
|
||||||
<p><strong>WarpBox</strong> was made by <strong>Daniel Legt</strong>.</p>
|
|
||||||
<p>Temporary file boxes, terminal-friendly uploads, and old-web UI charm.</p>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
examples: {
|
|
||||||
title: "Examples",
|
|
||||||
html: `
|
|
||||||
<h3>Upload examples</h3>
|
|
||||||
<h4>Basic CLI upload</h4>
|
|
||||||
<pre>curl \\
|
|
||||||
-F 'files=@./photo.png' \\
|
|
||||||
-F 'retention=24h' \\
|
|
||||||
${window.location.origin}/upload</pre>
|
|
||||||
<h4>Multiple files with password</h4>
|
|
||||||
<pre>curl \\
|
|
||||||
-F 'files=@./one.png' \\
|
|
||||||
-F 'files=@./two.zip' \\
|
|
||||||
-F 'retention=1h' \\
|
|
||||||
-F 'password=secret-pass' \\
|
|
||||||
${window.location.origin}/upload</pre>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function openDoc(name) {
|
function popupTemplateData(name) {
|
||||||
const doc = docs[name];
|
const data = { origin: window.location.origin };
|
||||||
|
if (name !== "dailyQuota") return data;
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
boxLimit: maxBoxBytes ? formatBytes(maxBoxBytes) : "No configured limit",
|
||||||
|
boxPercent: maxBoxBytes ? Math.min(100, Math.round((totalBytes() / maxBoxBytes) * 100)) : 0,
|
||||||
|
fileLimit: maxFileBytes ? formatBytes(maxFileBytes) : "No configured limit",
|
||||||
|
filePercent: oversizedFiles().length ? 100 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDoc(name) {
|
||||||
|
try {
|
||||||
|
const doc = await window.WBPopups.renderDoc(name, popupTemplateData(name));
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
openPopup(doc.title, doc.html, doc.about);
|
openPopup(doc.title, doc.html, doc.about);
|
||||||
setStatus(`${doc.title} opened`);
|
setStatus(`${doc.title} opened`);
|
||||||
|
} catch (error) {
|
||||||
|
showToast(error.message || "Could not load help window.", "error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
|
if (announceDisabledReason(event)) return;
|
||||||
|
|
||||||
const menuButton = event.target.closest(".menu-button");
|
const menuButton = event.target.closest(".menu-button");
|
||||||
if (menuButton) {
|
if (menuButton) {
|
||||||
const item = menuButton.closest(".menu-item");
|
const item = menuButton.closest(".menu-item");
|
||||||
@@ -1067,19 +1055,16 @@ document.addEventListener("click", (event) => {
|
|||||||
}
|
}
|
||||||
if (action === "help" || action === "side-help") openDoc("faq");
|
if (action === "help" || action === "side-help") openDoc("faq");
|
||||||
if (action === "terminal-help") el.terminal?.focus();
|
if (action === "terminal-help") el.terminal?.focus();
|
||||||
if (action === "coming-soon") showToast("That shortcut is decorative for now.");
|
if (action === "coming-soon") showToast("Coming Soon, not implemented just yet.");
|
||||||
if (action === "side-close" || action === "side-folder-close" || action === "fake-close" || action === "minimize" || action === "toggle-fit") showToast("Window controls are decorative on this page.");
|
if (action === "fake-close") showToast("Close button denied. The upload window is staying open.", "warning");
|
||||||
return;
|
if (action === "minimize") showToast("Minimize requested. WarpBox stays visible so your queue is safe.");
|
||||||
|
if (action === "toggle-fit") {
|
||||||
|
document.body.classList.toggle("fit-window");
|
||||||
|
showToast("Maximize requested. The pixel rectangle feels important now.");
|
||||||
}
|
}
|
||||||
|
if (action === "side-close") showToast("Box Options refuses to leave. Settings stay visible.");
|
||||||
const expiry = event.target.closest("[data-expiry]")?.dataset.expiry;
|
if (action === "side-help") showToast("Terminal help opened. Copy the command and feed it files.");
|
||||||
if (expiry && el.expiry) {
|
if (action === "side-folder-close") showToast("The folder window saw that click and chose denial.");
|
||||||
el.expiry.value = expiry;
|
|
||||||
syncZipForRetention();
|
|
||||||
saveSettings();
|
|
||||||
syncMenuChecks();
|
|
||||||
updateTerminal();
|
|
||||||
setStatus(`Expiry set to ${event.target.textContent.trim()}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1115,6 +1100,22 @@ document.addEventListener("click", (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", (event) => {
|
||||||
|
announceDisabledReason(event);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
document.querySelectorAll(".menu-item").forEach((item) => {
|
||||||
|
item.addEventListener("mouseenter", () => {
|
||||||
|
if (!document.querySelector(".menu-item.is-open")) return;
|
||||||
|
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
|
||||||
|
node.classList.remove("is-open");
|
||||||
|
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
|
item.classList.add("is-open");
|
||||||
|
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
el.fileInput?.addEventListener("change", () => addFiles(el.fileInput.files));
|
el.fileInput?.addEventListener("change", () => addFiles(el.fileInput.files));
|
||||||
|
|
||||||
[el.dropSurface, el.dropzone].filter(Boolean).forEach((target) => {
|
[el.dropSurface, el.dropzone].filter(Boolean).forEach((target) => {
|
||||||
@@ -1150,7 +1151,11 @@ el.modalBackdrop?.addEventListener("click", closeDoc);
|
|||||||
[el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.allowZip, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean).forEach((control) => {
|
[el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.allowZip, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean).forEach((control) => {
|
||||||
control.addEventListener("input", () => {
|
control.addEventListener("input", () => {
|
||||||
if (control === el.boxName) syncSlugFromName();
|
if (control === el.boxName) syncSlugFromName();
|
||||||
if (control === el.customSlug) el.customSlug.dataset.auto = "false";
|
if (control === el.customSlug) {
|
||||||
|
const clean = sanitizeSlugInput(el.customSlug.value);
|
||||||
|
if (el.customSlug.value !== clean) el.customSlug.value = clean;
|
||||||
|
el.customSlug.dataset.auto = "false";
|
||||||
|
}
|
||||||
if (control === el.apiKeyInput) validateApiKeyField();
|
if (control === el.apiKeyInput) validateApiKeyField();
|
||||||
saveSettings();
|
saveSettings();
|
||||||
updateTerminal();
|
updateTerminal();
|
||||||
|
|||||||
36
static/js/upload-popups.js
Normal file
36
static/js/upload-popups.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
window.WBPopups = (() => {
|
||||||
|
const cache = new Map();
|
||||||
|
const docs = {
|
||||||
|
cli: { title: "CLI Guide", template: "cli" },
|
||||||
|
faq: { title: "Help & FAQ", template: "faq" },
|
||||||
|
dailyQuota: { title: "Upload limits", template: "upload-limits" },
|
||||||
|
about: { title: "About WarpBox", template: "about", about: true },
|
||||||
|
examples: { title: "Examples", template: "examples" },
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadTemplate(name) {
|
||||||
|
if (cache.has(name)) return cache.get(name);
|
||||||
|
const response = await fetch(`/static/popups/${name}.html`, { credentials: "same-origin" });
|
||||||
|
if (!response.ok) throw new Error(`Could not load popup template: ${name}`);
|
||||||
|
const template = await response.text();
|
||||||
|
cache.set(name, template);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderTemplate(name, data = {}) {
|
||||||
|
const template = await loadTemplate(name);
|
||||||
|
return window.WBUtils.renderTemplate(template, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderDoc(name, data = {}) {
|
||||||
|
const doc = docs[name];
|
||||||
|
if (!doc) return null;
|
||||||
|
return {
|
||||||
|
title: doc.title,
|
||||||
|
about: Boolean(doc.about),
|
||||||
|
html: await renderTemplate(doc.template, data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { renderTemplate, renderDoc };
|
||||||
|
})();
|
||||||
9
static/js/upload-utils.js
Normal file
9
static/js/upload-utils.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
window.WBUtils = (() => {
|
||||||
|
function renderTemplate(template, data = {}) {
|
||||||
|
return String(template).replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
|
||||||
|
return Object.prototype.hasOwnProperty.call(data, key) ? String(data[key]) : "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { renderTemplate };
|
||||||
|
})();
|
||||||
3
static/popups/about.html
Normal file
3
static/popups/about.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<h3>WarpBox</h3>
|
||||||
|
<p><strong>WarpBox</strong> was made by <strong>Daniel Legt</strong>.</p>
|
||||||
|
<p>Temporary file boxes, terminal-friendly uploads, and old-web UI charm.</p>
|
||||||
6
static/popups/clear.html
Normal file
6
static/popups/clear.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<h3>Confirm clear</h3>
|
||||||
|
<p>This removes the current queue, resets progress, and unlocks the Start upload button.</p>
|
||||||
|
<div class="copy-fallback-actions">
|
||||||
|
<button class="win98-button" type="button" id="confirm-clear-yes">Clear</button>
|
||||||
|
<button class="win98-button" type="button" id="confirm-clear-no">Cancel</button>
|
||||||
|
</div>
|
||||||
9
static/popups/cli.html
Normal file
9
static/popups/cli.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<h3>Upload with cURL</h3>
|
||||||
|
<p>WarpBox accepts normal multipart form uploads through the compatibility endpoint:</p>
|
||||||
|
<pre>curl \
|
||||||
|
-F 'files=@./my-file.zip' \
|
||||||
|
-F 'retention=1h' \
|
||||||
|
{{ origin }}/upload
|
||||||
|
</pre>
|
||||||
|
<h4>Browser flow</h4>
|
||||||
|
<p>The browser uses the manifest API: it creates a box, uploads each file, and marks failed uploads so the download page does not wait forever.</p>
|
||||||
7
static/popups/copy-failed.html
Normal file
7
static/popups/copy-failed.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<h3>Clipboard access failed</h3>
|
||||||
|
<p>The browser refused clipboard access. Copy it manually from the field below.</p>
|
||||||
|
<textarea class="copy-fallback-text" readonly>{{ value }}</textarea>
|
||||||
|
<div class="copy-fallback-actions">
|
||||||
|
{{ openLink }}
|
||||||
|
<button class="win98-button" type="button" id="fallback-close">Close</button>
|
||||||
|
</div>
|
||||||
8
static/popups/duplicate.html
Normal file
8
static/popups/duplicate.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<h3>Duplicate file names detected</h3>
|
||||||
|
<p>These files have the same names as files already in the queue.</p>
|
||||||
|
<ol class="duplicate-list">{{ list }}</ol>
|
||||||
|
<p>Skip them, or append numbers so they become names like <code>file (2).zip</code>.</p>
|
||||||
|
<div class="copy-fallback-actions">
|
||||||
|
<button class="win98-button" type="button" id="duplicate-append">Append numbers</button>
|
||||||
|
<button class="win98-button" type="button" id="duplicate-skip">Skip duplicates</button>
|
||||||
|
</div>
|
||||||
102
static/popups/examples.html
Normal file
102
static/popups/examples.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<h3>Upload examples</h3>
|
||||||
|
<h4>Basic CLI upload</h4>
|
||||||
|
<pre>curl \
|
||||||
|
-F 'files=@./photo.png' \
|
||||||
|
-F 'retention=24h' \
|
||||||
|
{{ origin }}/upload
|
||||||
|
</pre>
|
||||||
|
<h4>Multiple files with password</h4>
|
||||||
|
<pre>curl \
|
||||||
|
-F 'files=@./one.png' \
|
||||||
|
-F 'files=@./two.zip' \
|
||||||
|
-F 'retention=1h' \
|
||||||
|
-F 'password=secret-pass' \
|
||||||
|
{{ origin }}/upload
|
||||||
|
</pre>
|
||||||
|
<h4>Go</h4>
|
||||||
|
<pre>package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
file, err := os.Open("photo.png")
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
part, err := writer.CreateFormFile("files", "photo.png")
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
if _, err := io.Copy(part, file); err != nil { panic(err) }
|
||||||
|
_ = writer.WriteField("retention", "1h")
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "{{ origin }}/upload", &body)
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
defer resp.Body.Close()
|
||||||
|
out, _ := io.ReadAll(resp.Body)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<h4>Java 11+ HttpClient</h4>
|
||||||
|
<pre>import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class UploadWarpBox {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
String boundary = "----WarpBoxBoundary" + System.currentTimeMillis();
|
||||||
|
Path file = Path.of("photo.png");
|
||||||
|
byte[] prefix = ("--" + boundary + "\r\n" +
|
||||||
|
"Content-Disposition: form-data; name=\"retention\"\r\n\r\n" +
|
||||||
|
"1h\r\n" +
|
||||||
|
"--" + boundary + "\r\n" +
|
||||||
|
"Content-Disposition: form-data; name=\"files\"; filename=\"photo.png\"\r\n" +
|
||||||
|
"Content-Type: application/octet-stream\r\n\r\n").getBytes();
|
||||||
|
byte[] suffix = ("\r\n--" + boundary + "--\r\n").getBytes();
|
||||||
|
byte[] fileBytes = Files.readAllBytes(file);
|
||||||
|
byte[] body = new byte[prefix.length + fileBytes.length + suffix.length];
|
||||||
|
System.arraycopy(prefix, 0, body, 0, prefix.length);
|
||||||
|
System.arraycopy(fileBytes, 0, body, prefix.length, fileBytes.length);
|
||||||
|
System.arraycopy(suffix, 0, body, prefix.length + fileBytes.length, suffix.length);
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("{{ origin }}/upload"))
|
||||||
|
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = HttpClient.newHttpClient()
|
||||||
|
.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
System.out.println(response.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
<h4>JavaScript Node.js</h4>
|
||||||
|
<pre>import { openAsBlob } from 'node:fs';
|
||||||
|
|
||||||
|
const file = await openAsBlob('./photo.png');
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('files', file, 'photo.png');
|
||||||
|
form.append('retention', '1h');
|
||||||
|
|
||||||
|
const res = await fetch('{{ origin }}/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(await res.text());
|
||||||
|
</pre>
|
||||||
17
static/popups/faq.html
Normal file
17
static/popups/faq.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<h3>Help & FAQ</h3>
|
||||||
|
<section class="shortcut-section">
|
||||||
|
<h4>Keyboard shortcuts</h4>
|
||||||
|
<ul class="shortcut-list">
|
||||||
|
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">O</span></span><span>Browse for files.</span></li>
|
||||||
|
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">U</span></span><span>Start the current upload.</span></li>
|
||||||
|
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">K</span></span><span>Copy the full cURL command.</span></li>
|
||||||
|
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">L</span></span><span>Copy the share URL after upload.</span></li>
|
||||||
|
<li><span><span class="kbd">F1</span></span><span>Open this window.</span></li>
|
||||||
|
<li><span><span class="kbd">Esc</span></span><span>Close menus and popups.</span></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<div class="faq-list">
|
||||||
|
<div class="faq-item"><p><strong>Can I password protect uploads?</strong></p><p>Yes. Set a password in Box Options before starting the upload.</p></div>
|
||||||
|
<div class="faq-item"><p><strong>What happens if one file fails?</strong></p><p>The failed row stays red, successful files remain available, and WarpBox marks the failed file in the manifest.</p></div>
|
||||||
|
<div class="faq-item"><p><strong>Are all options server-backed?</strong></p><p>Expiry, password, ZIP download, and one-time download are sent to the backend. Notes like box name, custom slug, and API key mode are saved locally until backend support exists.</p></div>
|
||||||
|
</div>
|
||||||
12
static/popups/upload-limits.html
Normal file
12
static/popups/upload-limits.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<h3>Upload limits</h3>
|
||||||
|
<div class="quota-meter-list">
|
||||||
|
<div class="quota-meter">
|
||||||
|
<div class="quota-meter-head"><span>Box size</span><span>{{ boxLimit }}</span></div>
|
||||||
|
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:{{ boxPercent }}%"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="quota-meter">
|
||||||
|
<div class="quota-meter-head"><span>Single file</span><span>{{ fileLimit }}</span></div>
|
||||||
|
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:{{ filePercent }}%"></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="quota-note">These values come from the running WarpBox configuration.</p>
|
||||||
5
static/popups/warning.html
Normal file
5
static/popups/warning.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<h3>{{ title }}</h3>
|
||||||
|
{{ content }}
|
||||||
|
<div class="copy-fallback-actions">
|
||||||
|
<button class="win98-button" type="button" id="fallback-close">OK</button>
|
||||||
|
</div>
|
||||||
@@ -40,11 +40,8 @@
|
|||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<button class="menu-button" type="button" aria-expanded="false"><u>B</u>ox</button>
|
<button class="menu-button" type="button" aria-expanded="false"><u>B</u>ox</button>
|
||||||
<div class="menu-popup" role="menu">
|
<div class="menu-popup" role="menu">
|
||||||
{{ range .RetentionOptions }}
|
<button class="menu-action" type="button" data-action="toggle-delete-once"><img src="/static/img/icons/recycle_bin_full_cool-5.png" alt="" aria-hidden="true"><span>One-time download</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-expiry="{{ .Key }}"><span class="menu-check" data-expiry-check="{{ .Key }}">{{ if eq .Key $.DefaultRetention }}✓{{ end }}</span><span>{{ .Label }}</span><span></span></button>
|
<button class="menu-action" type="button" data-action="copy-link"><img src="/static/img/sprites/frame_web-0.png" alt="" aria-hidden="true"><span>Copy share URL</span><span></span></button>
|
||||||
{{ end }}
|
|
||||||
<div class="menu-separator"></div>
|
|
||||||
<button class="menu-action" type="button" data-action="toggle-delete-once"><span class="menu-check" data-delete-once-check></span><span>One-time download</span><span></span></button>
|
|
||||||
<button class="menu-action" type="button" data-doc="dailyQuota"><img src="/static/img/icons/scanner_alt-3.png" alt="" aria-hidden="true"><span>Upload limits...</span><span></span></button>
|
<button class="menu-action" type="button" data-doc="dailyQuota"><img src="/static/img/icons/scanner_alt-3.png" alt="" aria-hidden="true"><span>Upload limits...</span><span></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,16 +50,16 @@
|
|||||||
<div class="menu-popup" role="menu">
|
<div class="menu-popup" role="menu">
|
||||||
<button class="menu-action" type="button" data-action="random-password"><img src="/static/img/sprites/file_padlock.png" alt="" aria-hidden="true"><span>Generate password</span><span></span></button>
|
<button class="menu-action" type="button" data-action="random-password"><img src="/static/img/sprites/file_padlock.png" alt="" aria-hidden="true"><span>Generate password</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-action="random-box-name"><img src="/static/img/icons/directory_closed-2.png" alt="" aria-hidden="true"><span>Random box name</span><span></span></button>
|
<button class="menu-action" type="button" data-action="random-box-name"><img src="/static/img/icons/directory_closed-2.png" alt="" aria-hidden="true"><span>Random box name</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-action="clear-password"><span>○</span><span>Clear password</span><span></span></button>
|
<button class="menu-action" type="button" data-action="clear-password"><img src="/static/img/icons/x_mark_pixel.png" alt="" aria-hidden="true"><span>Clear password</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-action="toggle-page"><span class="menu-check" data-download-page-check>✓</span><span>Download page</span><span></span></button>
|
<button class="menu-action" type="button" data-action="toggle-page"><img src="/static/img/sprites/frame_web-0.png" alt="" aria-hidden="true"><span>Download page</span><span></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<button class="menu-button" type="button" aria-expanded="false"><u>H</u>elp</button>
|
<button class="menu-button" type="button" aria-expanded="false"><u>H</u>elp</button>
|
||||||
<div class="menu-popup" role="menu">
|
<div class="menu-popup" role="menu">
|
||||||
<button class="menu-action" type="button" data-action="help"><img src="/static/img/icons/tip.png" alt="" aria-hidden="true"><span>Show quick help</span><span>F1</span></button>
|
<button class="menu-action" type="button" data-action="help"><img src="/static/img/icons/tip.png" alt="" aria-hidden="true"><span>Show quick help</span><span>F1</span></button>
|
||||||
<button class="menu-action" type="button" data-action="terminal-help"><span>></span><span>Show cURL command</span><span></span></button>
|
<button class="menu-action" type="button" data-action="terminal-help"><img src="/static/img/icons/shell_window1.png" alt="" aria-hidden="true"><span>Show cURL command</span><span></span></button>
|
||||||
<button class="menu-action" type="button" data-doc="about"><span>i</span><span>About WarpBox</span><span></span></button>
|
<button class="menu-action" type="button" data-doc="about"><img src="/static/WarpBoxLogo.png" alt="" aria-hidden="true"><span>About WarpBox</span><span></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -81,7 +78,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<label class="upload-dropzone" for="file-upload" tabindex="0" id="dropzone">
|
<label class="upload-dropzone" for="file-upload" tabindex="1" id="dropzone" data-disabled-reason="The current box is sealed after upload. Press Clear to start a new box.">
|
||||||
<img class="upload-icon-img" src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true">
|
<img class="upload-icon-img" src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true">
|
||||||
<span class="upload-primary">{{ if .UploadsEnabled }}Drop files here{{ else }}Guest uploads disabled{{ end }}</span>
|
<span class="upload-primary">{{ if .UploadsEnabled }}Drop files here{{ else }}Guest uploads disabled{{ end }}</span>
|
||||||
<span class="upload-secondary">or <span class="upload-linklike">click to browse</span> from your computer</span>
|
<span class="upload-secondary">or <span class="upload-linklike">click to browse</span> from your computer</span>
|
||||||
@@ -113,7 +110,7 @@
|
|||||||
|
|
||||||
<div class="upload-actions">
|
<div class="upload-actions">
|
||||||
<button class="win98-button" type="button" data-action="clear">Clear</button>
|
<button class="win98-button" type="button" data-action="clear">Clear</button>
|
||||||
<button class="win98-button start-upload-cta" type="submit" id="start-button" {{ if not .UploadsEnabled }}disabled{{ end }} data-disabled-reason="Start upload is unavailable right now.">Start upload</button>
|
<button class="win98-button start-upload-cta" type="submit" id="start-button" tabindex="4" {{ if not .UploadsEnabled }}disabled{{ end }} data-disabled-reason="Start upload is unavailable right now.">Start upload</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -136,7 +133,7 @@
|
|||||||
<div class="box-options-form" id="box-options-form">
|
<div class="box-options-form" id="box-options-form">
|
||||||
<label class="option-row">
|
<label class="option-row">
|
||||||
<span>Expires:</span>
|
<span>Expires:</span>
|
||||||
<select class="upload-select" id="expiry-select" name="retention_key">
|
<select class="upload-select" id="expiry-select" name="retention_key" tabindex="2">
|
||||||
{{ range .RetentionOptions }}
|
{{ range .RetentionOptions }}
|
||||||
<option value="{{ .Key }}" {{ if eq .Key $.DefaultRetention }}selected{{ end }}>{{ .Label }}</option>
|
<option value="{{ .Key }}" {{ if eq .Key $.DefaultRetention }}selected{{ end }}>{{ .Label }}</option>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -144,7 +141,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="option-row">
|
<label class="option-row">
|
||||||
<span>Password:</span>
|
<span>Password:</span>
|
||||||
<input class="upload-text-input" id="password-input" type="text" placeholder="optional" autocomplete="off">
|
<input class="upload-text-input" id="password-input" type="text" placeholder="optional" autocomplete="off" tabindex="3">
|
||||||
</label>
|
</label>
|
||||||
<label class="option-row">
|
<label class="option-row">
|
||||||
<span>Max views:</span>
|
<span>Max views:</span>
|
||||||
@@ -152,11 +149,11 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="option-row">
|
<label class="option-row">
|
||||||
<span>Box name:</span>
|
<span>Box name:</span>
|
||||||
<input class="upload-text-input" id="box-name" type="text" maxlength="42" placeholder="local note">
|
<input class="upload-text-input" id="box-name" type="text" maxlength="42" placeholder="optional, normal text">
|
||||||
</label>
|
</label>
|
||||||
<label class="option-row">
|
<label class="option-row">
|
||||||
<span>Custom slug:</span>
|
<span>Custom slug:</span>
|
||||||
<input class="upload-text-input" id="custom-slug" type="text" maxlength="32" placeholder="local note">
|
<input class="upload-text-input" id="custom-slug" type="text" maxlength="32" pattern="[a-z0-9-]*" placeholder="optional-slug">
|
||||||
</label>
|
</label>
|
||||||
<label class="option-check">
|
<label class="option-check">
|
||||||
<input type="checkbox" id="download-page" checked>
|
<input type="checkbox" id="download-page" checked>
|
||||||
@@ -196,7 +193,7 @@
|
|||||||
<section class="win98-window side-panel">
|
<section class="win98-window side-panel">
|
||||||
<div class="win98-titlebar">
|
<div class="win98-titlebar">
|
||||||
<div class="win98-titlebar-label">
|
<div class="win98-titlebar-label">
|
||||||
<img class="win98-titlebar-icon" src="/static/img/sprites/computer_explorer-4.png" alt="" aria-hidden="true">
|
<img class="win98-titlebar-icon" src="/static/img/icons/shell_window1.png" alt="" aria-hidden="true">
|
||||||
<h2>Terminal Upload</h2>
|
<h2>Terminal Upload</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-help" title="Help-ish">?</button></div>
|
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-help" title="Help-ish">?</button></div>
|
||||||
@@ -221,7 +218,7 @@
|
|||||||
<button class="folder-icon-button" type="button" data-doc="cli"><img src="/static/img/icons/directory_closed-2.png" alt="" aria-hidden="true"><span>CLI Guide</span></button>
|
<button class="folder-icon-button" type="button" data-doc="cli"><img src="/static/img/icons/directory_closed-2.png" alt="" aria-hidden="true"><span>CLI Guide</span></button>
|
||||||
<button class="folder-icon-button" type="button" data-doc="faq"><img src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true"><span>Help & FAQ</span></button>
|
<button class="folder-icon-button" type="button" data-doc="faq"><img src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true"><span>Help & FAQ</span></button>
|
||||||
<button class="folder-icon-button" type="button" data-doc="examples"><img src="/static/img/icons/shell_window1.png" alt="" aria-hidden="true"><span>Examples</span></button>
|
<button class="folder-icon-button" type="button" data-doc="examples"><img src="/static/img/icons/shell_window1.png" alt="" aria-hidden="true"><span>Examples</span></button>
|
||||||
<button class="folder-icon-button folder-icon-button-disabled" type="button" data-action="coming-soon"><img src="/static/img/sprites/eject_pc_cool-5.png" alt="" aria-hidden="true"><span>WarpBox.exe</span></button>
|
<button class="folder-icon-button folder-icon-button-disabled" type="button" data-action="coming-soon"><img src="/static/WarpBoxLogo.png" alt="" aria-hidden="true"><span>WarpBox.exe</span></button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -241,6 +238,8 @@
|
|||||||
</section>
|
</section>
|
||||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<script src="/static/js/upload-utils.js"></script>
|
||||||
|
<script src="/static/js/upload-popups.js"></script>
|
||||||
<script src="/static/js/app.js"></script>
|
<script src="/static/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user