Compare commits
4 Commits
cb026d4fd1
...
e330fb04b3
| Author | SHA1 | Date | |
|---|---|---|---|
| e330fb04b3 | |||
| a8c0666b5a | |||
| 6035ea1eb2 | |||
| 82acaffdd8 |
@@ -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",
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ const boxAuthCookiePrefix = "warpbox_box_"
|
|||||||
|
|
||||||
var oneTimeDownloadLocks sync.Map
|
var oneTimeDownloadLocks sync.Map
|
||||||
|
|
||||||
|
func formatBrowserTime(value time.Time) string {
|
||||||
|
if value.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value.UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
func (app *App) handleIndex(ctx *gin.Context) {
|
func (app *App) handleIndex(ctx *gin.Context) {
|
||||||
ctx.HTML(http.StatusOK, "index.html", gin.H{
|
ctx.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
"RetentionOptions": app.retentionOptions(),
|
"RetentionOptions": app.retentionOptions(),
|
||||||
@@ -63,6 +70,7 @@ func (app *App) handleShowBox(ctx *gin.Context) {
|
|||||||
"PollMS": app.config.BoxPollIntervalMS,
|
"PollMS": app.config.BoxPollIntervalMS,
|
||||||
"RetentionLabel": manifest.RetentionLabel,
|
"RetentionLabel": manifest.RetentionLabel,
|
||||||
"ExpiresAt": manifest.ExpiresAt,
|
"ExpiresAt": manifest.ExpiresAt,
|
||||||
|
"ExpiresAtISO": formatBrowserTime(manifest.ExpiresAt),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +148,8 @@ func (app *App) handleBoxStatus(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, ok := app.authorizeBoxRequest(ctx, boxID, false); !ok {
|
manifest, _, ok := app.authorizeBoxRequest(ctx, boxID, false)
|
||||||
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +159,7 @@ func (app *App) handleBoxStatus(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "files": files})
|
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "expires_at": formatBrowserTime(manifest.ExpiresAt), "files": files})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) handleDownloadBox(ctx *gin.Context) {
|
func (app *App) handleDownloadBox(ctx *gin.Context) {
|
||||||
@@ -516,14 +525,43 @@ func (app *App) handleLegacyUpload(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
savedFiles := make([]models.BoxFile, 0, len(files))
|
retentionKey := strings.TrimSpace(ctx.PostForm("retention_key"))
|
||||||
|
if retentionKey == "" {
|
||||||
|
retentionKey = strings.TrimSpace(ctx.PostForm("retention"))
|
||||||
|
}
|
||||||
|
allowZip := true
|
||||||
|
if strings.EqualFold(strings.TrimSpace(ctx.PostForm("allow_zip")), "false") {
|
||||||
|
allowZip = false
|
||||||
|
}
|
||||||
|
request := models.CreateBoxRequest{
|
||||||
|
RetentionKey: retentionKey,
|
||||||
|
Password: ctx.PostForm("password"),
|
||||||
|
AllowZip: &allowZip,
|
||||||
|
Files: make([]models.CreateBoxFileRequest, 0, len(files)),
|
||||||
|
}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
savedFile, err := boxstore.SaveUpload(boxID, file)
|
request.Files = append(request.Files, models.CreateBoxFileRequest{Name: file.Filename, Size: file.Size})
|
||||||
|
}
|
||||||
|
if err := app.validateCreateBoxRequest(&request); err != nil {
|
||||||
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestFiles, err := boxstore.CreateManifest(boxID, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
savedFiles := make([]models.BoxFile, 0, len(files))
|
||||||
|
for index, file := range files {
|
||||||
|
savedFile, err := boxstore.SaveManifestUpload(boxID, manifestFiles[index].ID, file)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = boxstore.MarkFileStatus(boxID, manifestFiles[index].ID, models.FileStatusFailed)
|
||||||
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
savedFiles = append(savedFiles, savedFile)
|
savedFiles = append(savedFiles, savedFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
@@ -11,6 +11,10 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, rgba(255,255,255,.9), rgba(238,238,238,.58)),
|
||||||
|
repeating-linear-gradient(0deg, rgba(0,0,0,.025) 0 1px, transparent 1px 6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav {
|
.admin-nav {
|
||||||
@@ -35,6 +39,12 @@ body {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
background: #dfdfdf;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #808080;
|
||||||
|
border-bottom: 1px solid #808080;
|
||||||
|
box-shadow: inset 1px 1px 0 #f7f7f7, inset -1px -1px 0 #b0b0b0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-link strong,
|
.admin-link strong,
|
||||||
@@ -50,6 +60,10 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
border-top: 2px solid #808080;
|
||||||
|
border-left: 2px solid #808080;
|
||||||
|
border-right: 2px solid #ffffff;
|
||||||
|
border-bottom: 2px solid #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-table th,
|
.admin-table th,
|
||||||
@@ -74,7 +88,14 @@ body {
|
|||||||
.admin-form-row textarea,
|
.admin-form-row textarea,
|
||||||
.admin-form-row select {
|
.admin-form-row select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
min-height: 24px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #808080;
|
||||||
|
border-left: 1px solid #808080;
|
||||||
|
border-right: 1px solid #ffffff;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-checks {
|
.admin-checks {
|
||||||
@@ -103,4 +124,9 @@ body {
|
|||||||
|
|
||||||
.admin-summary span {
|
.admin-summary span {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
|
background: #dfdfdf;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #808080;
|
||||||
|
border-bottom: 1px solid #808080;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,82 +15,326 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'PixelOperatorMono';
|
font-family: 'MonoCraft';
|
||||||
src: url('/static/fonts/pixel_operator/PixelOperatorMono-Bold.ttf');
|
src: url('/static/fonts/Monocraft.ttf');
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PixeloidSans';
|
|
||||||
src: url('/static/fonts/pixeloid_sans/PixeloidSans.ttf');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PixeloidSans';
|
|
||||||
src: url('/static/fonts/pixeloid_sans/PixeloidSans-Bold.ttf');
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: 'PixeloidSans', 'PixelOperator', sans-serif, Arial, Helvetica;
|
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||||
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: 13px;
|
||||||
|
--ui-scale: 1;
|
||||||
--base-font-size: 14px;
|
|
||||||
|
|
||||||
/* Colours */
|
|
||||||
--w98-blue: #000078;
|
--w98-blue: #000078;
|
||||||
--w98-blue-gradient: linear-gradient(to right, #000078, 80%, #0f80cd);
|
--w98-blue-gradient: linear-gradient(90deg, #000078 0%, #000078 28%, #0f80cd 50%, #000078 72%, #000078 100%);
|
||||||
--w98-gray: #c0c0c0;
|
--w98-gray: #c0c0c0;
|
||||||
--w98-gray2: #a6a6a6;
|
--w98-gray2: #a6a6a6;
|
||||||
--w98-gray-gradient: linear-gradient(to bottom, #fff, 95%, #c0c0c0);
|
--ok: #008000;
|
||||||
|
--danger: #800000;
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
* {
|
||||||
button,
|
box-sizing: border-box;
|
||||||
label[for],
|
scrollbar-width: auto;
|
||||||
.win98-button:not(:disabled) {
|
scrollbar-color: #c0c0c0 #808080;
|
||||||
cursor: url('/static/cursors/vaporwave-hotline-white-plus/Link\ Select.cur'), auto;
|
image-rendering: pixelated;
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
input[type="password"],
|
|
||||||
input[type="file"],
|
|
||||||
textarea,
|
|
||||||
[contenteditable="true"] {
|
|
||||||
cursor: url('/static/cursors/vaporwave-hotline-white-plus/Hotline\ Black\ Handwriting.cur'), text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
min-height: 100%;
|
||||||
font-size: var(--base-font-size);
|
font-size: var(--base-font-size);
|
||||||
color: white;
|
color: #ffffff;
|
||||||
background-color: #000;
|
background: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
width: 100vw;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
height: auto;
|
overflow-x: hidden;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
background-image: url('/static/img/bg/stars1.gif');
|
background-image: url('/static/img/bg/stars1.gif');
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
|
background-size: auto;
|
||||||
|
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 100vw;
|
padding: 18px;
|
||||||
min-height: 100vh;
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
label[for],
|
||||||
|
.menu-button,
|
||||||
|
.win98-button:not(:disabled),
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="file"],
|
||||||
|
textarea {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px dotted #000078;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
background: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: repeating-linear-gradient(45deg, #c0c0c0 0 2px, #b5b5b5 2px 4px);
|
||||||
|
border-top: 1px solid #808080;
|
||||||
|
border-left: 1px solid #808080;
|
||||||
|
border-right: 1px solid #ffffff;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb,
|
||||||
|
::-webkit-scrollbar-button:single-button {
|
||||||
|
background: #c0c0c0;
|
||||||
|
border-top: 2px solid #ffffff;
|
||||||
|
border-left: 2px solid #ffffff;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-button {
|
||||||
|
min-width: 92px;
|
||||||
|
height: 28px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: #000000;
|
||||||
|
background: var(--w98-gray);
|
||||||
|
border-top: 2px solid #ffffff;
|
||||||
|
border-left: 2px solid #ffffff;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 13px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-button:disabled,
|
||||||
|
.win98-button[aria-disabled="true"],
|
||||||
|
button:disabled,
|
||||||
|
button[aria-disabled="true"],
|
||||||
|
input:disabled,
|
||||||
|
select:disabled,
|
||||||
|
textarea:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-button:disabled,
|
||||||
|
.win98-button[aria-disabled="true"] {
|
||||||
|
color: #808080;
|
||||||
|
text-shadow: 1px 1px 0 #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-button:active:not(:disabled):not([aria-disabled="true"]),
|
||||||
|
.win98-control:active,
|
||||||
|
.menu-button[aria-expanded="true"] {
|
||||||
|
border-top-color: #000000;
|
||||||
|
border-left-color: #000000;
|
||||||
|
border-right-color: #ffffff;
|
||||||
|
border-bottom-color: #ffffff;
|
||||||
|
box-shadow: inset -1px -1px 0 #dfdfdf, inset 1px 1px 0 #808080;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: none;
|
||||||
|
background: rgba(128, 128, 128, .42);
|
||||||
|
z-index: 70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop.is-visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-window {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: min(780px, calc(100vw - 24px));
|
||||||
|
max-height: min(760px, calc(100vh - 24px));
|
||||||
|
display: none;
|
||||||
|
z-index: 80;
|
||||||
|
zoom: var(--ui-scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-window.is-visible {
|
||||||
|
display: flex;
|
||||||
|
animation: popup-open-v10 180ms steps(5, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: calc(100vh - 90px);
|
||||||
|
margin: 0 6px 6px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, rgba(255,255,255,.9), rgba(238,238,238,.58)),
|
||||||
|
repeating-linear-gradient(0deg, rgba(0,0,0,.025) 0 1px, transparent 1px 6px);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body h3 { margin: 0 0 8px; font-size: 16px; line-height: 18px; }
|
||||||
|
.popup-body h4 { margin: 14px 0 6px; font-size: 14px; line-height: 16px; }
|
||||||
|
.popup-body p { margin: 0 0 8px; }
|
||||||
|
.popup-body ul,
|
||||||
|
.popup-body ol { margin: 0 0 8px 18px; padding: 0; }
|
||||||
|
.popup-body li { margin: 0 0 4px; }
|
||||||
|
.popup-body .code-block {
|
||||||
|
margin: 6px 0 10px;
|
||||||
|
padding: 8px 8px 22px;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow: auto;
|
||||||
|
color: #00ff66;
|
||||||
|
background: #000000;
|
||||||
|
border: 0;
|
||||||
|
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
white-space: pre;
|
||||||
|
user-select: text;
|
||||||
|
cursor: text;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body .code-block::after {
|
||||||
|
content: "\A";
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-fallback-text {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 58px;
|
||||||
|
font-family: 'MonoCraft', 'PixelOperatorMono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-window.is-properties-popup {
|
||||||
|
width: min(520px, calc(100vw - 24px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-window.is-preview-popup {
|
||||||
|
width: min(760px, calc(100vw - 24px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 52px;
|
||||||
|
max-width: min(360px, calc(100vw - 24px));
|
||||||
|
display: none;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffcc;
|
||||||
|
border-top: 2px solid #ffffff;
|
||||||
|
border-left: 2px solid #ffffff;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
z-index: 90;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 14px;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,.45);
|
||||||
|
zoom: var(--ui-scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.is-visible {
|
||||||
|
display: block;
|
||||||
|
animation: toast-in 180ms steps(3, end), toast-buzz 700ms steps(2, end) 180ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.toast-warning {
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffcc;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
border-image: repeating-linear-gradient(45deg, #111111 0 8px, #ffcc00 8px 16px) 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.toast-error {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #b00000;
|
||||||
|
text-shadow: 1px 1px 0 #000000;
|
||||||
|
border-color: #ffb0b0 #330000 #330000 #ffb0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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-buzz { 0%, 100% { margin-right: 0; } 25% { margin-right: 2px; } 50% { margin-right: -2px; } }
|
||||||
|
|
||||||
|
@media (min-width: 1800px) {
|
||||||
|
:root { --base-font-size: 14px; --ui-scale: 1.2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 2048px) {
|
||||||
|
:root { --base-font-size: 15px; --ui-scale: 1.36; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 2560px) {
|
||||||
|
:root { --base-font-size: 16px; --ui-scale: 1.58; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 3200px) {
|
||||||
|
:root { --base-font-size: 18px; --ui-scale: 1.88; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 1ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,48 @@
|
|||||||
.box-window {
|
.box-window {
|
||||||
width: 640px;
|
width: min(860px, calc(100vw - 36px));
|
||||||
height: 460px;
|
height: min(560px, calc(100vh - 36px));
|
||||||
|
zoom: var(--ui-scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-toolbar {
|
body.fit-window .box-window {
|
||||||
display: flex;
|
width: min(980px, calc(100vw / var(--ui-scale) - 20px));
|
||||||
|
height: min(720px, calc(100vh / var(--ui-scale) - 20px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-command-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: 40px;
|
min-height: 40px;
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-toolbar-button {
|
.box-toolbar-button {
|
||||||
width: 116px;
|
width: auto;
|
||||||
|
min-width: 158px;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-toolbar-button img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-address {
|
.box-address {
|
||||||
display: grid;
|
grid-column: 1;
|
||||||
grid-template-columns: 58px minmax(0, 1fr);
|
|
||||||
align-items: center;
|
|
||||||
height: 28px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 8px 6px;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-meta {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 58px minmax(0, 1fr);
|
|
||||||
align-items: center;
|
|
||||||
height: 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 8px 6px;
|
|
||||||
gap: 6px;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-address code {
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 22px;
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #000000;
|
color: #000000;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-top: 1px solid #808080;
|
border-top: 1px solid #808080;
|
||||||
@@ -57,6 +50,33 @@
|
|||||||
border-right: 1px solid #dfdfdf;
|
border-right: 1px solid #dfdfdf;
|
||||||
border-bottom: 1px solid #dfdfdf;
|
border-bottom: 1px solid #dfdfdf;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 13px;
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-window.popup-window {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-window.popup-window.is-visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-meta {
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 8px 6px;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-meta span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-panel {
|
.box-panel {
|
||||||
@@ -64,6 +84,10 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: 0 8px 8px;
|
margin: 0 8px 8px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, rgba(255,255,255,.9), rgba(238,238,238,.58)),
|
||||||
|
repeating-linear-gradient(0deg, rgba(0,0,0,.025) 0 1px, transparent 1px 6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-file-grid {
|
.box-file-grid {
|
||||||
@@ -81,7 +105,6 @@
|
|||||||
grid-template-rows: 34px 18px 28px;
|
grid-template-rows: 34px 18px 28px;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 8px 6px;
|
padding: 8px 6px;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -169,6 +192,98 @@
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.box-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 168px;
|
||||||
|
display: none;
|
||||||
|
padding: 2px;
|
||||||
|
background: var(--w98-gray);
|
||||||
|
border-top: 2px solid #ffffff;
|
||||||
|
border-left: 2px solid #ffffff;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
box-shadow: 3px 3px 0 rgba(0,0,0,.35);
|
||||||
|
z-index: 95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-context-menu.is-visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-context-menu button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 24px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 7px;
|
||||||
|
color: #000000;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 13px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-context-menu button:hover,
|
||||||
|
.box-context-menu button:focus-visible {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #000078;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-context-menu img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
object-fit: contain;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 92px minmax(0, 1fr);
|
||||||
|
gap: 7px 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #dfdfdf;
|
||||||
|
border-top: 1px solid #808080;
|
||||||
|
border-left: 1px solid #808080;
|
||||||
|
border-right: 1px solid #ffffff;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties-grid dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties-grid dd {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame {
|
||||||
|
width: min(680px, 100%);
|
||||||
|
min-height: 260px;
|
||||||
|
max-height: min(520px, calc(100vh - 160px));
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #000000;
|
||||||
|
border: 1px solid #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame.is-text {
|
||||||
|
min-height: 240px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #00ff66;
|
||||||
|
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
.box-empty {
|
.box-empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -185,6 +300,7 @@
|
|||||||
main {
|
main {
|
||||||
display: block;
|
display: block;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-window {
|
.box-window {
|
||||||
@@ -192,6 +308,7 @@
|
|||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
zoom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-titlebar {
|
.box-titlebar {
|
||||||
@@ -203,6 +320,15 @@
|
|||||||
height: 26px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.box-command-row {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-address {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.box-panel {
|
.box-panel {
|
||||||
margin: 0 6px 8px;
|
margin: 0 6px 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.login-window {
|
.login-window {
|
||||||
width: 420px;
|
width: 420px;
|
||||||
height: 248px;
|
height: 248px;
|
||||||
|
zoom: var(--ui-scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
@@ -14,7 +15,12 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #c0c0c0;
|
background-color: #dfdfdf;
|
||||||
|
background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.18) 0 1px, transparent 1px 5px);
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #808080;
|
||||||
|
border-bottom: 1px solid #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-alert {
|
.login-alert {
|
||||||
@@ -52,7 +58,6 @@
|
|||||||
.login-input {
|
.login-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -82,7 +87,6 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 8px 8px;
|
padding: 0 8px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +102,7 @@
|
|||||||
main {
|
main {
|
||||||
display: block;
|
display: block;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-window {
|
.login-window {
|
||||||
@@ -105,6 +110,7 @@
|
|||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
zoom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-titlebar {
|
.login-titlebar {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,14 @@
|
|||||||
.win98-window {
|
.win98-window {
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
background: var(--w98-gray);
|
background-color: #c0c0c0;
|
||||||
border-top: 2px solid #ffffff;
|
background-image: linear-gradient(180deg, rgba(255,255,255,.24), rgba(0,0,0,.06));
|
||||||
border-left: 2px solid #ffffff;
|
border-top: 1px solid #ffffff;
|
||||||
border-right: 2px solid #000000;
|
border-left: 1px solid #ffffff;
|
||||||
border-bottom: 2px solid #000000;
|
border-right: 1px solid #000000;
|
||||||
box-shadow:
|
border-bottom: 1px solid #000000;
|
||||||
inset -1px -1px 0 #808080,
|
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 5px 6px 0 rgba(0,0,0,.5);
|
||||||
inset 1px 1px 0 #dfdfdf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.win98-titlebar {
|
.win98-titlebar {
|
||||||
@@ -18,14 +16,23 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
padding: 2px 3px 2px 6px;
|
padding: 2px 3px 2px 6px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background: var(--w98-blue-gradient);
|
background: var(--w98-blue-gradient);
|
||||||
|
background-size: 240% 100%;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,255,255,.35), inset 0 -1px 0 rgba(0,0,0,.35);
|
||||||
|
user-select: none;
|
||||||
|
animation: titlebar-center-drift 34s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.win98-titlebar h1 {
|
@keyframes titlebar-center-drift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
100% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-titlebar h1,
|
||||||
|
.win98-titlebar h2 {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -60,36 +67,28 @@
|
|||||||
.win98-control {
|
.win98-control {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
box-sizing: border-box;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
background: var(--w98-gray);
|
background: var(--w98-gray);
|
||||||
border-top: 1px solid #ffffff;
|
border-top: 1px solid #ffffff;
|
||||||
border-left: 1px solid #ffffff;
|
border-left: 1px solid #ffffff;
|
||||||
border-right: 1px solid #000000;
|
border-right: 1px solid #000000;
|
||||||
border-bottom: 1px solid #000000;
|
border-bottom: 1px solid #000000;
|
||||||
box-shadow:
|
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
|
||||||
inset -1px -1px 0 #808080,
|
font-family: inherit;
|
||||||
inset 1px 1px 0 #dfdfdf;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.win98-menu {
|
.win98-minimize {
|
||||||
display: flex;
|
align-items: start;
|
||||||
align-items: center;
|
padding-top: 0;
|
||||||
gap: 18px;
|
line-height: 8px;
|
||||||
height: 22px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.win98-panel {
|
.win98-panel {
|
||||||
box-sizing: border-box;
|
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-top: 2px solid #808080;
|
border-top: 2px solid #808080;
|
||||||
border-left: 2px solid #808080;
|
border-left: 2px solid #808080;
|
||||||
@@ -97,51 +96,10 @@
|
|||||||
border-bottom: 2px solid #ffffff;
|
border-bottom: 2px solid #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.win98-button {
|
|
||||||
width: 92px;
|
|
||||||
height: 28px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 10px;
|
|
||||||
color: #000000;
|
|
||||||
background: var(--w98-gray);
|
|
||||||
border-top: 2px solid #ffffff;
|
|
||||||
border-left: 2px solid #ffffff;
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
box-shadow:
|
|
||||||
inset -1px -1px 0 #808080,
|
|
||||||
inset 1px 1px 0 #dfdfdf;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 13px;
|
|
||||||
text-align: center;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.win98-button:active {
|
|
||||||
border-top-color: #000000;
|
|
||||||
border-left-color: #000000;
|
|
||||||
border-right-color: #ffffff;
|
|
||||||
border-bottom-color: #ffffff;
|
|
||||||
box-shadow:
|
|
||||||
inset -1px -1px 0 #dfdfdf,
|
|
||||||
inset 1px 1px 0 #808080;
|
|
||||||
padding: 1px 9px 0 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.win98-button:focus-visible {
|
|
||||||
outline: 1px dotted #000000;
|
|
||||||
outline-offset: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.win98-statusbar {
|
.win98-statusbar {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 4px 4px;
|
padding: 0 4px 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
@@ -152,7 +110,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -161,3 +118,13 @@
|
|||||||
border-right: 1px solid #ffffff;
|
border-right: 1px solid #ffffff;
|
||||||
border-bottom: 1px solid #ffffff;
|
border-bottom: 1px solid #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.win98-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 13px;
|
||||||
|
}
|
||||||
|
|||||||
1477
static/js/app.js
1477
static/js/app.js
File diff suppressed because it is too large
Load Diff
274
static/js/box.js
274
static/js/box.js
@@ -1,20 +1,182 @@
|
|||||||
const boxPanel = document.querySelector(".box-panel[data-box-id]");
|
const boxPanel = document.querySelector(".box-panel[data-box-id]");
|
||||||
const boxStatus = document.querySelector(".box-statusbar span:first-child");
|
const boxStatus = document.querySelector(".box-statusbar span:first-child");
|
||||||
|
const boxAddress = document.querySelector("#box-address");
|
||||||
|
const boxExpiryMeta = document.querySelector(".box-meta[data-expires-at]");
|
||||||
|
const boxExpiryText = document.querySelector("#box-expiry-text");
|
||||||
|
const contextMenu = document.querySelector("#box-context-menu");
|
||||||
|
const docPopup = document.querySelector("#doc-popup");
|
||||||
|
const docPopupTitle = document.querySelector("#doc-popup-title");
|
||||||
|
const docPopupBody = document.querySelector("#doc-popup-body");
|
||||||
|
const docPopupClose = document.querySelector("#doc-popup-close");
|
||||||
|
const modalBackdrop = document.querySelector("#modal-backdrop");
|
||||||
|
const toast = document.querySelector("#toast");
|
||||||
const zipOnly = boxPanel && boxPanel.dataset.zipOnly === "true";
|
const zipOnly = boxPanel && boxPanel.dataset.zipOnly === "true";
|
||||||
|
|
||||||
document.querySelectorAll('.box-file[aria-disabled="true"]').forEach((item) => {
|
let contextFile = null;
|
||||||
item.addEventListener("click", (event) => {
|
|
||||||
if (item.getAttribute("aria-disabled") === "true") {
|
function htmlEscape(value) {
|
||||||
event.preventDefault();
|
return String(value || "")
|
||||||
}
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = "info") {
|
||||||
|
window.WarpBoxUI.toast(message, type, { target: toast });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPopup(title, html, options = {}) {
|
||||||
|
window.WarpBoxUI.openPopup(title, html, {
|
||||||
|
...options,
|
||||||
|
popup: docPopup,
|
||||||
|
title: docPopupTitle,
|
||||||
|
body: docPopupBody,
|
||||||
|
backdrop: modalBackdrop,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function closePopup() {
|
||||||
|
window.WarpBoxUI.closePopup({ popup: docPopup, backdrop: modalBackdrop });
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentExpiryDate() {
|
||||||
|
const value = boxExpiryMeta?.dataset.expiresAt || "";
|
||||||
|
if (!value) return null;
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(ms) {
|
||||||
|
if (ms <= 0) return "expired";
|
||||||
|
const totalSeconds = Math.ceil(ms / 1000);
|
||||||
|
const days = Math.floor(totalSeconds / 86400);
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
if (days) return `${days}d ${hours}h ${minutes}m`;
|
||||||
|
if (hours) return `${hours}h ${minutes}m ${seconds}s`;
|
||||||
|
if (minutes) return `${minutes}m ${seconds}s`;
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExpiryCountdown() {
|
||||||
|
if (!boxExpiryText || !boxExpiryMeta) return;
|
||||||
|
const expiry = currentExpiryDate();
|
||||||
|
if (!expiry) {
|
||||||
|
boxExpiryText.textContent = "Expires after one-time download";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boxExpiryText.textContent = `Expires in ${formatDuration(expiry.getTime() - Date.now())}`;
|
||||||
|
boxExpiryText.title = `Expires at ${expiry.toLocaleString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenu?.classList.remove("is-visible");
|
||||||
|
contextMenu?.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContextMenu(file, x, y) {
|
||||||
|
if (!contextMenu) return;
|
||||||
|
contextFile = file;
|
||||||
|
contextMenu.style.left = `${Math.min(x, window.innerWidth - 190)}px`;
|
||||||
|
contextMenu.style.top = `${Math.min(y, window.innerHeight - 98)}px`;
|
||||||
|
contextMenu.classList.add("is-visible");
|
||||||
|
contextMenu.setAttribute("aria-hidden", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileData(item) {
|
||||||
|
return {
|
||||||
|
id: item.dataset.fileId || "",
|
||||||
|
name: item.dataset.name || item.querySelector(".box-file-name")?.textContent || "",
|
||||||
|
size: item.dataset.size || "",
|
||||||
|
mime: item.dataset.mime || "",
|
||||||
|
status: item.dataset.status || "",
|
||||||
|
statusLabel: item.querySelector(".box-file-meta")?.textContent || "",
|
||||||
|
downloadPath: item.dataset.downloadPath || item.getAttribute("href") || "",
|
||||||
|
thumbnail: item.dataset.thumbnail || "",
|
||||||
|
canDownload: item.getAttribute("aria-disabled") !== "true" && item.getAttribute("href") !== "#",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(item) {
|
||||||
|
const data = fileData(item);
|
||||||
|
if (!data.canDownload) {
|
||||||
|
showToast(zipOnly ? "Individual file downloads are disabled for one-time boxes. Use Download Zip." : "This file is not ready for download yet.", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = data.downloadPath;
|
||||||
|
setTimeout(refreshBoxStatus, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewURL(data) {
|
||||||
|
return data.canDownload ? data.downloadPath : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewFile(item) {
|
||||||
|
const data = fileData(item);
|
||||||
|
if (zipOnly) {
|
||||||
|
showToast("Previews are disabled for one-time boxes. Use Download Zip.", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = previewURL(data);
|
||||||
|
if (!url) {
|
||||||
|
showToast("This file is not ready to preview yet.", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mime = data.mime.toLowerCase();
|
||||||
|
const name = htmlEscape(data.name);
|
||||||
|
if (mime.startsWith("image/")) {
|
||||||
|
openPopup(`${data.name} preview`, `<img class="preview-frame" src="${htmlEscape(url)}" alt="${name}">`, { preview: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mime.startsWith("video/")) {
|
||||||
|
openPopup(`${data.name} preview`, `<video class="preview-frame" src="${htmlEscape(url)}" controls></video>`, { preview: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mime.startsWith("audio/")) {
|
||||||
|
openPopup(`${data.name} preview`, `<audio class="preview-frame" src="${htmlEscape(url)}" controls></audio>`, { preview: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mime === "application/pdf") {
|
||||||
|
openPopup(`${data.name} preview`, `<iframe class="preview-frame" src="${htmlEscape(url)}" title="${name}"></iframe>`, { preview: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mime.startsWith("text/") || /\.(txt|md|json|csv|log|html|css|js)$/i.test(data.name)) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error("Preview failed");
|
||||||
|
const text = await response.text();
|
||||||
|
openPopup(`${data.name} preview`, `<code class="code-block preview-frame is-text">${htmlEscape(text.slice(0, 120000))}</code>`, { preview: true });
|
||||||
|
} catch (_) {
|
||||||
|
showToast("The browser could not load a text preview.", "error");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast("This file type cannot be previewed in the browser.", "warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProperties(item) {
|
||||||
|
const data = fileData(item);
|
||||||
|
const url = data.downloadPath ? new URL(data.downloadPath, window.location.origin).toString() : "Not ready";
|
||||||
|
openPopup(`${data.name} Properties`, `
|
||||||
|
<h3>${htmlEscape(data.name)}</h3>
|
||||||
|
<dl class="properties-grid">
|
||||||
|
<dt>Name</dt><dd>${htmlEscape(data.name)}</dd>
|
||||||
|
<dt>Size</dt><dd>${htmlEscape(data.size || "Unknown")}</dd>
|
||||||
|
<dt>Type</dt><dd>${htmlEscape(data.mime || "Unknown")}</dd>
|
||||||
|
<dt>Status</dt><dd>${htmlEscape(data.statusLabel || data.status || "Unknown")}</dd>
|
||||||
|
<dt>File ID</dt><dd>${htmlEscape(data.id)}</dd>
|
||||||
|
<dt>Location</dt><dd>${htmlEscape(url)}</dd>
|
||||||
|
</dl>
|
||||||
|
`, { properties: true });
|
||||||
|
}
|
||||||
|
|
||||||
function updateBoxFile(file) {
|
function updateBoxFile(file) {
|
||||||
const item = document.querySelector(`.box-file[data-file-id="${file.id}"]`);
|
const item = document.querySelector(`.box-file[data-file-id="${file.id}"]`);
|
||||||
if (!item) {
|
if (!item) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = item.querySelector(".box-file-meta");
|
const meta = item.querySelector(".box-file-meta");
|
||||||
const icon = item.querySelector(".box-file-icon");
|
const icon = item.querySelector(".box-file-icon");
|
||||||
@@ -26,6 +188,11 @@ function updateBoxFile(file) {
|
|||||||
item.classList.toggle("is-loading", !isComplete && !isFailed);
|
item.classList.toggle("is-loading", !isComplete && !isFailed);
|
||||||
item.classList.toggle("has-thumbnail", Boolean(file.thumbnail_path));
|
item.classList.toggle("has-thumbnail", Boolean(file.thumbnail_path));
|
||||||
item.dataset.status = file.status;
|
item.dataset.status = file.status;
|
||||||
|
item.dataset.name = file.name || item.dataset.name || "";
|
||||||
|
item.dataset.size = file.size_label || item.dataset.size || "";
|
||||||
|
item.dataset.mime = file.mime_type || item.dataset.mime || "";
|
||||||
|
item.dataset.downloadPath = file.download_path || item.dataset.downloadPath || "";
|
||||||
|
item.dataset.thumbnail = file.thumbnail_path || "";
|
||||||
item.title = file.title;
|
item.title = file.title;
|
||||||
|
|
||||||
if (isComplete && !zipOnly) {
|
if (isComplete && !zipOnly) {
|
||||||
@@ -38,27 +205,22 @@ function updateBoxFile(file) {
|
|||||||
item.setAttribute("aria-disabled", "true");
|
item.setAttribute("aria-disabled", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta) {
|
if (meta) meta.textContent = `${file.status_label} · ${file.size_label}`;
|
||||||
meta.textContent = `${file.status_label} · ${file.size_label}`;
|
if (icon) icon.src = file.thumbnail_path || file.icon_path;
|
||||||
}
|
|
||||||
|
|
||||||
if (icon) {
|
|
||||||
icon.src = file.thumbnail_path || file.icon_path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshBoxStatus() {
|
async function refreshBoxStatus() {
|
||||||
if (!boxPanel) {
|
if (!boxPanel) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const boxID = boxPanel.dataset.boxId;
|
const boxID = boxPanel.dataset.boxId;
|
||||||
const response = await fetch(`/box/${boxID}/status`);
|
const response = await fetch(`/box/${boxID}/status`);
|
||||||
if (!response.ok) {
|
if (!response.ok) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
if (boxExpiryMeta && typeof result.expires_at === "string") {
|
||||||
|
boxExpiryMeta.dataset.expiresAt = result.expires_at;
|
||||||
|
updateExpiryCountdown();
|
||||||
|
}
|
||||||
result.files.forEach(updateBoxFile);
|
result.files.forEach(updateBoxFile);
|
||||||
|
|
||||||
if (boxStatus) {
|
if (boxStatus) {
|
||||||
@@ -73,17 +235,73 @@ async function refreshBoxStatus() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const action = event.target.closest("[data-action]")?.dataset.action;
|
||||||
|
if (action === "fake-close") showToast("Close clicked. The download window is emotionally attached.", "warning");
|
||||||
|
if (action === "minimize") showToast("Minimize clicked. WarpBox refuses to disappear quietly.");
|
||||||
|
if (action === "toggle-fit") {
|
||||||
|
document.body.classList.toggle("fit-window");
|
||||||
|
showToast("Maximize clicked. The window is doing its best.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextAction = event.target.closest("[data-context-action]")?.dataset.contextAction;
|
||||||
|
if (contextAction && contextFile) {
|
||||||
|
event.preventDefault();
|
||||||
|
const item = contextFile;
|
||||||
|
closeContextMenu();
|
||||||
|
if (contextAction === "preview") previewFile(item);
|
||||||
|
if (contextAction === "download") downloadFile(item);
|
||||||
|
if (contextAction === "properties") showProperties(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.target.closest("#box-context-menu")) closeContextMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".box-file").forEach((item) => {
|
||||||
|
item.addEventListener("click", (event) => {
|
||||||
|
if (item.getAttribute("aria-disabled") === "true") {
|
||||||
|
event.preventDefault();
|
||||||
|
showToast(zipOnly ? "Individual file downloads are disabled for one-time boxes. Use Download Zip." : "This file is not ready yet.", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(refreshBoxStatus, 900);
|
||||||
|
});
|
||||||
|
item.addEventListener("contextmenu", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
showContextMenu(item, event.clientX, event.clientY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
boxAddress?.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(window.location.href);
|
||||||
|
showToast("Current box URL copied.");
|
||||||
|
} catch (_) {
|
||||||
|
openPopup("Copy box URL", `<p>Clipboard access failed. Copy this URL manually.</p><textarea class="copy-fallback-text" readonly>${htmlEscape(window.location.href)}</textarea>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
docPopupClose?.addEventListener("click", closePopup);
|
||||||
|
modalBackdrop?.addEventListener("click", closePopup);
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closePopup();
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateExpiryCountdown();
|
||||||
|
setInterval(updateExpiryCountdown, 1000);
|
||||||
|
|
||||||
if (boxPanel) {
|
if (boxPanel) {
|
||||||
const pollMS = Number.parseInt(boxPanel.dataset.pollMs, 10) || 5000;
|
const pollMS = Number.parseInt(boxPanel.dataset.pollMs, 10) || 5000;
|
||||||
const timer = setInterval(async () => {
|
const timer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const hasLoadingFiles = await refreshBoxStatus();
|
const hasLoadingFiles = await refreshBoxStatus();
|
||||||
if (!hasLoadingFiles) {
|
if (!hasLoadingFiles) clearInterval(timer);
|
||||||
clearInterval(timer);
|
} catch (_) {
|
||||||
}
|
// Keep polling through temporary network/server hiccups.
|
||||||
} catch (error) {
|
|
||||||
// Keep polling through temporary network/server hiccups; otherwise
|
|
||||||
// an in-progress file can appear stuck forever after one bad poll.
|
|
||||||
}
|
}
|
||||||
}, pollMS);
|
}, pollMS);
|
||||||
}
|
}
|
||||||
|
|||||||
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 };
|
||||||
|
})();
|
||||||
11
static/js/upload-utils.js
Normal file
11
static/js/upload-utils.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
window.WBUtils = (() => {
|
||||||
|
function renderTemplate(template, data = {}) {
|
||||||
|
return window.WarpBoxUI?.renderTemplate
|
||||||
|
? window.WarpBoxUI.renderTemplate(template, data)
|
||||||
|
: String(template).replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
|
||||||
|
return Object.prototype.hasOwnProperty.call(data, key) ? String(data[key]) : "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { renderTemplate };
|
||||||
|
})();
|
||||||
48
static/js/warpbox-ui.js
Normal file
48
static/js/warpbox-ui.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
window.WarpBoxUI = (() => {
|
||||||
|
let toastTimer = null;
|
||||||
|
|
||||||
|
function toast(message, type = "info", options = {}) {
|
||||||
|
const target = options.target || document.querySelector("#toast");
|
||||||
|
if (!target) return;
|
||||||
|
target.textContent = message;
|
||||||
|
target.classList.remove("toast-info", "toast-warning", "toast-error", "is-visible");
|
||||||
|
target.classList.add(`toast-${type}`, "is-visible");
|
||||||
|
clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(() => target.classList.remove("is-visible"), options.duration || 2600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function popupElements(options = {}) {
|
||||||
|
return {
|
||||||
|
popup: options.popup || document.querySelector("#doc-popup"),
|
||||||
|
title: options.title || document.querySelector("#doc-popup-title"),
|
||||||
|
body: options.body || document.querySelector("#doc-popup-body"),
|
||||||
|
backdrop: options.backdrop || document.querySelector("#modal-backdrop"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPopup(titleText, html, options = {}) {
|
||||||
|
const parts = popupElements(options);
|
||||||
|
if (!parts.popup || !parts.title || !parts.body) return;
|
||||||
|
parts.title.textContent = titleText;
|
||||||
|
parts.body.innerHTML = html;
|
||||||
|
parts.popup.classList.toggle("is-about-popup", Boolean(options.about));
|
||||||
|
parts.popup.classList.toggle("is-properties-popup", Boolean(options.properties));
|
||||||
|
parts.popup.classList.toggle("is-preview-popup", Boolean(options.preview));
|
||||||
|
parts.popup.classList.add("is-visible");
|
||||||
|
parts.backdrop?.classList.add("is-visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePopup(options = {}) {
|
||||||
|
const parts = popupElements(options);
|
||||||
|
parts.popup?.classList.remove("is-visible", "is-about-popup", "is-properties-popup", "is-preview-popup");
|
||||||
|
parts.backdrop?.classList.remove("is-visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { toast, openPopup, closePopup, renderTemplate };
|
||||||
|
})();
|
||||||
6
static/popups/about.html
Normal file
6
static/popups/about.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="about-popup-content">
|
||||||
|
<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>
|
||||||
|
<p><strong>Version:</strong> v1.3.8a</p>
|
||||||
|
</div>
|
||||||
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>
|
||||||
33
static/popups/cli.html
Normal file
33
static/popups/cli.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<h3>Upload with cURL</h3>
|
||||||
|
<p>WarpBox accepts normal multipart form uploads through the compatibility endpoint:</p>
|
||||||
|
<code class="code-block">curl \
|
||||||
|
-F 'files=@./my-file.zip' \
|
||||||
|
-F 'retention=1h' \
|
||||||
|
{{ origin }}/upload
|
||||||
|
</code>
|
||||||
|
<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>
|
||||||
|
<h4>Make a WarpBox executable</h4>
|
||||||
|
<p>Save this as <code>warpbox</code>, make it executable, and put it somewhere on your <code>PATH</code>.</p>
|
||||||
|
<code class="code-block">#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
echo "Usage: warpbox FILE [FILE ...]" >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
endpoint="${WARPBOX_URL:-{{ origin }}}/upload"
|
||||||
|
retention="${WARPBOX_RETENTION:-1h}"
|
||||||
|
|
||||||
|
args=(-F "retention=${retention}")
|
||||||
|
for file in "$@"; do
|
||||||
|
args+=(-F "files=@${file}")
|
||||||
|
done
|
||||||
|
|
||||||
|
curl "${args[@]}" "${endpoint}"
|
||||||
|
</code>
|
||||||
|
<code class="code-block">chmod +x ./warpbox
|
||||||
|
sudo install -m 755 ./warpbox /usr/local/bin/warpbox
|
||||||
|
warpbox ./my-file.zip
|
||||||
|
</code>
|
||||||
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>
|
||||||
|
<code class="code-block">curl \
|
||||||
|
-F 'files=@./photo.png' \
|
||||||
|
-F 'retention=24h' \
|
||||||
|
{{ origin }}/upload
|
||||||
|
</code>
|
||||||
|
<h4>Multiple files with password</h4>
|
||||||
|
<code class="code-block">curl \
|
||||||
|
-F 'files=@./one.png' \
|
||||||
|
-F 'files=@./two.zip' \
|
||||||
|
-F 'retention=1h' \
|
||||||
|
-F 'password=secret-pass' \
|
||||||
|
{{ origin }}/upload
|
||||||
|
</code>
|
||||||
|
<h4>Go</h4>
|
||||||
|
<code class="code-block">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))
|
||||||
|
}
|
||||||
|
</code>
|
||||||
|
<h4>Java 11+ HttpClient</h4>
|
||||||
|
<code class="code-block">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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code>
|
||||||
|
<h4>JavaScript Node.js</h4>
|
||||||
|
<code class="code-block">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());
|
||||||
|
</code>
|
||||||
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>
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>WarpBox - {{ .BoxID }}</title>
|
<title>WarpBox - {{ .BoxID }}</title>
|
||||||
<link rel="icon" type="image/png" href="/static/WarpBoxLogo.png">
|
<link rel="icon" type="image/png" href="/static/WarpBoxLogo.png">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="/static/css/app.css">
|
<link rel="stylesheet" href="/static/css/app.css">
|
||||||
<link rel="stylesheet" href="/static/css/window.css">
|
<link rel="stylesheet" href="/static/css/window.css">
|
||||||
<link rel="stylesheet" href="/static/css/box.css">
|
<link rel="stylesheet" href="/static/css/box.css">
|
||||||
@@ -19,37 +18,24 @@
|
|||||||
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
||||||
<h1 id="box-window-title">WarpBox Explorer - {{ .BoxID }}</h1>
|
<h1 id="box-window-title">WarpBox Explorer - {{ .BoxID }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="win98-window-controls" aria-hidden="true">
|
<div class="win98-window-controls" aria-label="Window controls">
|
||||||
<span class="win98-control">_</span>
|
<button class="win98-control win98-minimize" type="button" data-action="minimize" title="Minimize" aria-label="Minimize">_</button>
|
||||||
<span class="win98-control">□</span>
|
<button class="win98-control" type="button" data-action="toggle-fit" title="Fit window" aria-label="Maximize">□</button>
|
||||||
<span class="win98-control">×</span>
|
<button class="win98-control" type="button" data-action="fake-close" title="Close" aria-label="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="win98-menu box-menu" aria-hidden="true">
|
<div class="box-command-row">
|
||||||
<span class="win98-menu-option">File</span>
|
<button class="box-address" type="button" id="box-address" data-copy-url="{{ .BoxID }}" title="Copy current page URL">{{ .BoxID }}</button>
|
||||||
<span class="win98-menu-option">Edit</span>
|
<a class="win98-button box-toolbar-button" href="/"><img src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true"><span>Upload</span></a>
|
||||||
<span class="win98-menu-option">View</span>
|
|
||||||
<span class="win98-menu-option">Tools</span>
|
|
||||||
<span class="win98-menu-option">Help</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box-toolbar">
|
|
||||||
<a class="win98-button box-toolbar-button" href="/">Upload</a>
|
|
||||||
{{ if .DownloadAll }}
|
{{ if .DownloadAll }}
|
||||||
<a class="win98-button box-toolbar-button" href="{{ .DownloadAll }}">Download Zip</a>
|
<a class="win98-button box-toolbar-button" href="{{ .DownloadAll }}"><img src="/static/img/icons/Windows Icons - PNG/zipfldr.dll_14_101-0.png" alt="" aria-hidden="true"><span>Download Zip</span></a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box-address">
|
|
||||||
<span>Address</span>
|
|
||||||
<code>/box/{{ .BoxID }}</code>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ if .RetentionLabel }}
|
{{ if .RetentionLabel }}
|
||||||
<div class="box-meta">
|
<div class="box-meta" data-expires-at="{{ .ExpiresAtISO }}">
|
||||||
<span>Retention</span>
|
<span id="box-expiry-text">Expires in {{ .RetentionLabel }}</span>
|
||||||
<span>{{ .RetentionLabel }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
@@ -57,7 +43,7 @@
|
|||||||
{{ if .Files }}
|
{{ if .Files }}
|
||||||
<div class="box-file-grid" aria-label="Uploaded files">
|
<div class="box-file-grid" aria-label="Uploaded files">
|
||||||
{{ range .Files }}
|
{{ range .Files }}
|
||||||
<a class="box-file {{ if .IsComplete }}is-complete{{ else if eq .Status "failed" }}is-failed{{ else }}is-loading{{ end }} {{ if .ThumbnailURL }}has-thumbnail{{ end }}" href="{{ if and .IsComplete (not $.ZipOnly) }}{{ .DownloadPath }}{{ else }}#{{ end }}" title="{{ if $.ZipOnly }}Available in ZIP download{{ else }}{{ .Title }}{{ end }}" data-file-id="{{ .ID }}" data-status="{{ .Status }}" {{ if and .IsComplete (not $.ZipOnly) }}download{{ else }}aria-disabled="true"{{ end }}>
|
<a class="box-file {{ if .IsComplete }}is-complete{{ else if eq .Status "failed" }}is-failed{{ else }}is-loading{{ end }} {{ if .ThumbnailURL }}has-thumbnail{{ end }}" href="{{ if and .IsComplete (not $.ZipOnly) }}{{ .DownloadPath }}{{ else }}#{{ end }}" title="{{ if $.ZipOnly }}Available in ZIP download{{ else }}{{ .Title }}{{ end }}" data-file-id="{{ .ID }}" data-status="{{ .Status }}" data-name="{{ .Name }}" data-size="{{ .SizeLabel }}" data-mime="{{ .MimeType }}" data-download-path="{{ .DownloadPath }}" data-thumbnail="{{ .ThumbnailURL }}" {{ if and .IsComplete (not $.ZipOnly) }}download{{ else }}aria-disabled="true"{{ end }}>
|
||||||
<img class="box-file-icon" src="{{ if .ThumbnailURL }}{{ .ThumbnailURL }}{{ else }}{{ .IconPath }}{{ end }}" alt="" aria-hidden="true">
|
<img class="box-file-icon" src="{{ if .ThumbnailURL }}{{ .ThumbnailURL }}{{ else }}{{ .IconPath }}{{ end }}" alt="" aria-hidden="true">
|
||||||
<span class="box-file-name">{{ .Name }}</span>
|
<span class="box-file-name">{{ .Name }}</span>
|
||||||
<span class="box-file-meta">{{ .StatusLabel }} · {{ .SizeLabel }}</span>
|
<span class="box-file-meta">{{ .StatusLabel }} · {{ .SizeLabel }}</span>
|
||||||
@@ -75,6 +61,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<div class="modal-backdrop" id="modal-backdrop"></div>
|
||||||
|
<section class="win98-window popup-window" id="doc-popup" aria-modal="true" role="dialog" aria-labelledby="doc-popup-title">
|
||||||
|
<div class="win98-titlebar">
|
||||||
|
<div class="win98-titlebar-label">
|
||||||
|
<img class="win98-titlebar-icon" src="/static/img/icons/tip.png" alt="" aria-hidden="true">
|
||||||
|
<h2 id="doc-popup-title">WarpBox</h2>
|
||||||
|
</div>
|
||||||
|
<div class="win98-window-controls"><button class="win98-control popup-close" type="button" id="doc-popup-close" title="Close">×</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="win98-panel popup-body" id="doc-popup-body"></div>
|
||||||
|
</section>
|
||||||
|
<div class="box-context-menu" id="box-context-menu" role="menu" aria-hidden="true">
|
||||||
|
<button type="button" data-context-action="preview"><img src="/static/img/sprites/bitmap.png" alt="" aria-hidden="true"><span>Preview</span></button>
|
||||||
|
<button type="button" data-context-action="download"><img src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true"><span>Download</span></button>
|
||||||
|
<button type="button" data-context-action="properties"><img src="/static/img/icons/tip.png" alt="" aria-hidden="true"><span>Properties</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||||
|
<script src="/static/js/warpbox-ui.js"></script>
|
||||||
<script src="/static/js/box.js"></script>
|
<script src="/static/js/box.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,107 +3,241 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Warpbox</title>
|
<title>WarpBox</title>
|
||||||
<link rel="icon" type="image/png" href="/static/WarpBoxLogo.png">
|
<link rel="icon" type="image/png" href="/static/WarpBoxLogo.png">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="/static/css/app.css">
|
<link rel="stylesheet" href="/static/css/app.css">
|
||||||
<link rel="stylesheet" href="/static/css/window.css">
|
<link rel="stylesheet" href="/static/css/window.css">
|
||||||
<link rel="stylesheet" href="/static/css/upload.css">
|
<link rel="stylesheet" href="/static/css/upload.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main class="upload-main">
|
||||||
<section class="win98-window upload-window" aria-labelledby="upload-window-title">
|
<div class="desktop-wrap">
|
||||||
<header class="win98-titlebar upload-titlebar">
|
<section class="win98-window upload-window" aria-label="WarpBox upload window">
|
||||||
|
<div class="win98-titlebar upload-titlebar">
|
||||||
<div class="win98-titlebar-label">
|
<div class="win98-titlebar-label">
|
||||||
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
||||||
<h1 id="upload-window-title">WarpBox Upload</h1>
|
<h1>WarpBox</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="win98-window-controls" aria-hidden="true">
|
<div class="win98-window-controls" aria-label="Window controls">
|
||||||
<span class="win98-control">_</span>
|
<button class="win98-control win98-minimize" type="button" data-action="minimize" title="Minimize" aria-label="Minimize">_</button>
|
||||||
<span class="win98-control">□</span>
|
<button class="win98-control" type="button" data-action="toggle-fit" title="Fit window" aria-label="Maximize">□</button>
|
||||||
<span class="win98-control">×</span>
|
<button class="win98-control" type="button" data-action="fake-close" title="Close" aria-label="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="menu-bar" aria-label="Upload menu">
|
||||||
|
<div class="menu-item">
|
||||||
|
<button class="menu-button" type="button" aria-expanded="false"><u>F</u>ile</button>
|
||||||
|
<div class="menu-popup" role="menu">
|
||||||
|
<button class="menu-action" type="button" data-action="browse"><img src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true"><span>Add files...</span><span class="shortcut">Ctrl+O</span></button>
|
||||||
|
<button class="menu-action" type="button" data-action="start-upload"><img src="/static/img/icons/check_mark_pixel.png" alt="" aria-hidden="true"><span>Start upload</span><span class="shortcut">Ctrl+U</span></button>
|
||||||
|
<div class="menu-separator"></div>
|
||||||
|
<button class="menu-action" type="button" data-action="clear"><img src="/static/img/icons/x_mark_pixel.png" alt="" aria-hidden="true"><span>Clear queue</span><span></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<button class="menu-button" type="button" aria-expanded="false"><u>B</u>ox</button>
|
||||||
|
<div class="menu-popup" role="menu">
|
||||||
|
<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-action="copy-link" aria-disabled="true" data-disabled-reason="There is no share URL yet. Start an upload first."><img src="/static/img/sprites/frame_web-0.png" alt="" aria-hidden="true"><span>Copy share URL</span><span class="shortcut">Ctrl+L</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 class="menu-item">
|
||||||
|
<button class="menu-button" type="button" aria-expanded="false"><u>O</u>ptions</button>
|
||||||
|
<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-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"><img src="/static/img/icons/x_mark_pixel.png" alt="" aria-hidden="true"><span>Clear password</span><span></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<button class="menu-button" type="button" aria-expanded="false"><u>H</u>elp</button>
|
||||||
|
<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-doc="about"><img src="/static/WarpBoxLogo.png" alt="" aria-hidden="true"><span>About WarpBox</span><span></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<form class="upload-form" id="upload-form" action="/box" method="post" data-uploads-enabled="{{ if .UploadsEnabled }}true{{ else }}false{{ end }}" data-max-file-bytes="{{ .MaxFileSizeBytes }}" data-max-box-bytes="{{ .MaxBoxSizeBytes }}" data-default-retention="{{ .DefaultRetention }}">
|
||||||
|
<div class="win98-panel upload-panel" id="drop-surface">
|
||||||
|
<header class="upload-header">
|
||||||
|
<div>
|
||||||
|
<p class="upload-heading">Upload files</p>
|
||||||
|
<p class="upload-subtext">Drop files into this window or browse from your computer. WarpBox creates a temporary share link when the upload finishes.</p>
|
||||||
|
</div>
|
||||||
|
<aside class="upload-quota" aria-label="Box space">
|
||||||
|
<strong>Box space</strong>
|
||||||
|
<span id="box-space-text">0 B</span>
|
||||||
|
<span class="upload-quota-track" aria-hidden="true"><span class="upload-quota-bar" id="box-space-bar"></span></span>
|
||||||
|
</aside>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form class="upload-form" action="/upload" method="post" enctype="multipart/form-data" data-uploads-enabled="{{ if .UploadsEnabled }}true{{ else }}false{{ end }}">
|
<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.">
|
||||||
<div class="win98-menu upload-menu" aria-hidden="true">
|
<img class="upload-icon-img" src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true">
|
||||||
<span class="win98-menu-option">File</span>
|
<span class="upload-primary">{{ if .UploadsEnabled }}Drop files here{{ else }}Guest uploads disabled{{ end }}</span>
|
||||||
<span class="win98-menu-option">Edit</span>
|
<span class="upload-secondary">or <span class="upload-linklike">click to browse</span> from your computer</span>
|
||||||
<span class="win98-menu-option">View</span>
|
<span class="upload-secondary" id="limit-hint">Links expire automatically</span>
|
||||||
<span class="win98-menu-option">Help</span>
|
<input class="upload-input" id="file-upload" name="files" type="file" multiple {{ if not .UploadsEnabled }}disabled{{ end }} data-disabled-reason="Guest uploads are disabled.">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="win98-panel upload-panel">
|
|
||||||
<label class="upload-dropzone" for="file-upload">
|
|
||||||
<span class="upload-icon" aria-hidden="true"></span>
|
|
||||||
<span class="upload-primary">Drop file here</span>
|
|
||||||
<span class="upload-secondary">or click Browse to choose one</span>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input id="file-upload" class="upload-input" type="file" name="files" multiple {{ if not .UploadsEnabled }}disabled{{ end }}>
|
<div class="upload-details">
|
||||||
|
<span class="upload-detail-label">Queue:</span>
|
||||||
|
<span id="queue-label">{{ if .UploadsEnabled }}No files selected{{ else }}Guest uploads are disabled{{ end }}</span>
|
||||||
|
<span class="upload-file-count" id="queue-size">0 B total</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<fieldset class="upload-options">
|
<div class="upload-file-list" aria-label="Upload queue" id="file-list">
|
||||||
<legend>Box options</legend>
|
<p class="upload-empty-state">{{ if .UploadsEnabled }}No files in the box yet. Drop files here, use File > Add files, or click the dropzone.{{ else }}Guest uploads are disabled.{{ end }}</p>
|
||||||
<label class="upload-option-row" for="upload-retention">
|
</div>
|
||||||
<span>Keep files</span>
|
|
||||||
<select id="upload-retention" class="upload-select" name="retention">
|
<div class="upload-result">
|
||||||
|
<span class="upload-result-label">Share URL:</span>
|
||||||
|
<a class="upload-result-link is-empty" id="share-link" href="#" aria-disabled="true">Not created yet</a>
|
||||||
|
<button class="win98-button upload-share-button" type="button" id="copy-button" aria-disabled="true" data-disabled-reason="There is no share URL yet. Start an upload first.">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-overall">
|
||||||
|
<span class="upload-overall-track" aria-label="Overall upload progress"><span class="upload-overall-bar" id="overall-bar"></span></span>
|
||||||
|
<span class="upload-overall-percent" id="overall-percent">0%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-actions">
|
||||||
|
<button class="win98-button" type="button" data-action="clear">Clear</button>
|
||||||
|
<button class="win98-button start-upload-cta" type="submit" id="start-button" tabindex="4" aria-disabled="true" data-disabled-reason="There are no files selected. Please select files to upload.">Start upload</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="win98-statusbar upload-statusbar">
|
||||||
|
<span id="status-text">{{ if .UploadsEnabled }}Ready · drag files anywhere onto the window{{ else }}Guest uploads are disabled{{ end }}</span>
|
||||||
|
<span>WarpBox</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="side-stack" aria-label="Secondary upload panels">
|
||||||
|
<section class="win98-window side-panel">
|
||||||
|
<div class="win98-titlebar">
|
||||||
|
<div class="win98-titlebar-label">
|
||||||
|
<img class="win98-titlebar-icon" src="/static/img/icons/scanner_alt-3.png" alt="" aria-hidden="true">
|
||||||
|
<h2>Box Options</h2>
|
||||||
|
</div>
|
||||||
|
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-close" title="Close-ish">×</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="win98-panel side-body">
|
||||||
|
<div class="box-options-form" id="box-options-form">
|
||||||
|
<label class="option-row">
|
||||||
|
<span>Expires:</span>
|
||||||
|
<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 }}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="upload-check-row" for="upload-password-enabled">
|
<label class="option-row">
|
||||||
<input id="upload-password-enabled" type="checkbox">
|
<span>Password:</span>
|
||||||
<span>Password protect this box</span>
|
<input class="upload-text-input" id="password-input" type="text" placeholder="optional" autocomplete="off" tabindex="3">
|
||||||
</label>
|
</label>
|
||||||
<input id="upload-password" class="upload-text-input" type="password" autocomplete="new-password" placeholder="Password" disabled>
|
<label class="option-row">
|
||||||
<label class="upload-check-row" for="upload-zip-enabled">
|
<span>Max views:</span>
|
||||||
<input id="upload-zip-enabled" type="checkbox" checked>
|
<input class="upload-text-input" id="max-views" type="number" min="1" max="9999" placeholder="local note" data-disabled-reason="">
|
||||||
|
</label>
|
||||||
|
<label class="option-row">
|
||||||
|
<span>Box name:</span>
|
||||||
|
<input class="upload-text-input" id="box-name" type="text" maxlength="42" placeholder="optional, normal text">
|
||||||
|
</label>
|
||||||
|
<label class="option-row">
|
||||||
|
<span>Custom slug:</span>
|
||||||
|
<input class="upload-text-input" id="custom-slug" type="text" maxlength="32" pattern="[a-z0-9-]*" placeholder="optional-slug">
|
||||||
|
</label>
|
||||||
|
<label class="option-check">
|
||||||
|
<input type="checkbox" id="download-page" checked>
|
||||||
|
<span>Generate download page</span>
|
||||||
|
</label>
|
||||||
|
<label class="option-check">
|
||||||
|
<input type="checkbox" id="allow-zip" checked>
|
||||||
<span>Allow Download Zip</span>
|
<span>Allow Download Zip</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
<label class="option-check">
|
||||||
|
<input type="checkbox" id="allow-preview" checked>
|
||||||
<div class="upload-details">
|
<span>Allow previews when possible</span>
|
||||||
<span class="upload-detail-label">Selected Files</span>
|
</label>
|
||||||
<span id="upload-file-count" class="upload-file-count">0 files</span>
|
<label class="option-check">
|
||||||
</div>
|
<input type="checkbox" id="keep-filenames" checked>
|
||||||
|
<span>Keep original filenames</span>
|
||||||
<div class="upload-result is-hidden" aria-live="polite">
|
</label>
|
||||||
<span class="upload-result-label">Folder link</span>
|
<label class="option-check">
|
||||||
<a id="upload-box-link" class="upload-result-link is-empty" href="#" aria-disabled="true">Waiting for upload</a>
|
<input type="checkbox" id="private-box">
|
||||||
<button id="upload-share-button" class="win98-button upload-share-button" type="button" disabled>Share</button>
|
<span>Hide from public listings</span>
|
||||||
</div>
|
</label>
|
||||||
|
<label class="option-check">
|
||||||
<div class="upload-file-list" aria-live="polite" aria-label="Selected files">
|
<input type="checkbox" id="api-key-mode">
|
||||||
{{ if .UploadsEnabled }}
|
<span>Use API key for larger quota</span>
|
||||||
<p class="upload-empty-state">No files selected</p>
|
</label>
|
||||||
{{ else }}
|
<label class="option-row api-key-row" id="api-key-row">
|
||||||
<p class="upload-empty-state">Guest uploads are disabled.</p>
|
<span>API key:</span>
|
||||||
{{ end }}
|
<span class="api-key-field">
|
||||||
|
<input class="upload-text-input" id="api-key-input" type="password" placeholder="paste key when enabled" autocomplete="off" disabled data-disabled-reason="Enable Use API key for larger quota before typing an API key.">
|
||||||
|
<span class="api-key-state" id="api-key-state" aria-live="polite"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="upload-actions">
|
|
||||||
<label class="win98-button" for="file-upload">Browse...</label>
|
|
||||||
<button class="win98-button" type="submit" {{ if not .UploadsEnabled }}disabled{{ end }}>Upload</button>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<div class="upload-overall" aria-live="polite">
|
|
||||||
<div class="upload-overall-track" aria-hidden="true">
|
|
||||||
<span class="upload-overall-bar"></span>
|
|
||||||
</div>
|
|
||||||
<span class="upload-overall-percent">0%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="win98-statusbar upload-statusbar" aria-live="polite">
|
|
||||||
<span>Ready</span>
|
|
||||||
<span>WarpBox</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="win98-window side-panel">
|
||||||
|
<div class="win98-titlebar">
|
||||||
|
<div class="win98-titlebar-label">
|
||||||
|
<img class="win98-titlebar-icon" src="/static/img/icons/shell_window1.png" alt="" aria-hidden="true">
|
||||||
|
<h2>Terminal Upload</h2>
|
||||||
|
</div>
|
||||||
|
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-help" title="Help-ish">?</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="win98-panel side-body">
|
||||||
|
<div class="terminal-box" id="terminal-box" tabindex="0" aria-label="Terminal upload command"></div>
|
||||||
|
<div class="terminal-actions">
|
||||||
|
<button class="win98-button terminal-copy-button" type="button" id="copy-curl-button">Copy cURL command</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="win98-window helper-window" aria-label="WarpBox help folder">
|
||||||
|
<div class="win98-titlebar">
|
||||||
|
<div class="win98-titlebar-label">
|
||||||
|
<img class="win98-titlebar-icon" src="/static/img/icons/tip.png" alt="" aria-hidden="true">
|
||||||
|
<h2>WarpBox Help Folder</h2>
|
||||||
|
</div>
|
||||||
|
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-folder-close" title="Nope">×</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="win98-panel helper-body">
|
||||||
|
<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="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/WarpBoxLogo.png" alt="" aria-hidden="true"><span>WarpBox.exe</span></button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" id="modal-backdrop"></div>
|
||||||
|
<section class="win98-window popup-window" id="doc-popup" aria-modal="true" role="dialog" aria-labelledby="doc-popup-title">
|
||||||
|
<div class="win98-titlebar">
|
||||||
|
<div class="win98-titlebar-label">
|
||||||
|
<img class="win98-titlebar-icon" src="/static/img/icons/tip.png" alt="" aria-hidden="true">
|
||||||
|
<h2 id="doc-popup-title">WarpBox Help</h2>
|
||||||
|
</div>
|
||||||
|
<div class="win98-window-controls"><button class="win98-control popup-close" type="button" id="doc-popup-close" title="Close">×</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="win98-panel popup-body" id="doc-popup-body"></div>
|
||||||
|
</section>
|
||||||
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<script src="/static/js/warpbox-ui.js"></script>
|
||||||
|
<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>
|
||||||
|
|||||||
2449
warpbox_uploads_win98_dark_v16.html
Normal file
2449
warpbox_uploads_win98_dark_v16.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user