feat(ui): add clear queue flow and expose ISO expiry
- Add `formatBrowserTime()` and include ISO-8601 `expires_at` in box status JSON and `ExpiresAtISO` in the box view for browser-friendly rendering. - Refresh UI styling (switch to MonoCraft/PixelOperatorMono, tweak base font size) and treat `aria-disabled="true"` like `disabled` for consistent button states. - Introduce a clear-queue action with confirmation to reset upload state, unlock controls, and provide user feedback.feat(ui): add clear queue flow and expose ISO expiry - Add `formatBrowserTime()` and include ISO-8601 `expires_at` in box status JSON and `ExpiresAtISO` in the box view for browser-friendly rendering. - Refresh UI styling (switch to MonoCraft/PixelOperatorMono, tweak base font size) and treat `aria-disabled="true"` like `disabled` for consistent button states. - Introduce a clear-queue action with confirmation to reset upload state, unlock controls, and provide user feedback.
This commit is contained in:
@@ -21,6 +21,13 @@ const boxAuthCookiePrefix = "warpbox_box_"
|
||||
|
||||
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) {
|
||||
ctx.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"RetentionOptions": app.retentionOptions(),
|
||||
@@ -63,6 +70,7 @@ func (app *App) handleShowBox(ctx *gin.Context) {
|
||||
"PollMS": app.config.BoxPollIntervalMS,
|
||||
"RetentionLabel": manifest.RetentionLabel,
|
||||
"ExpiresAt": manifest.ExpiresAt,
|
||||
"ExpiresAtISO": formatBrowserTime(manifest.ExpiresAt),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,7 +148,8 @@ func (app *App) handleBoxStatus(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, _, ok := app.authorizeBoxRequest(ctx, boxID, false); !ok {
|
||||
manifest, _, ok := app.authorizeBoxRequest(ctx, boxID, false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,7 +159,7 @@ func (app *App) handleBoxStatus(ctx *gin.Context) {
|
||||
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) {
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'PixelOperator', 'MS Sans Serif', Arial, sans-serif;
|
||||
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||
font-smooth: never;
|
||||
-webkit-font-smoothing: none;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: geometricPrecision;
|
||||
image-rendering: pixelated;
|
||||
|
||||
--base-font-size: 14px;
|
||||
--base-font-size: 13px;
|
||||
--ui-scale: 1;
|
||||
--w98-blue: #000078;
|
||||
--w98-blue-gradient: linear-gradient(90deg, #000078 0%, #000078 28%, #0f80cd 50%, #000078 72%, #000078 100%);
|
||||
@@ -64,7 +64,7 @@ body {
|
||||
background-image: url('/static/img/bg/stars1.gif');
|
||||
background-repeat: repeat;
|
||||
background-size: auto;
|
||||
font-family: 'PixelOperator', 'MS Sans Serif', Arial, sans-serif;
|
||||
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -152,19 +152,22 @@ textarea {
|
||||
}
|
||||
|
||||
.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:disabled,
|
||||
.win98-button[aria-disabled="true"] {
|
||||
color: #808080;
|
||||
text-shadow: 1px 1px 0 #ffffff;
|
||||
}
|
||||
|
||||
.win98-button:active:not(:disabled),
|
||||
.win98-button:active:not(:disabled):not([aria-disabled="true"]),
|
||||
.win98-control:active,
|
||||
.menu-button[aria-expanded="true"] {
|
||||
border-top-color: #000000;
|
||||
@@ -175,20 +178,155 @@ textarea:disabled {
|
||||
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: 15px; --ui-scale: 1.2; }
|
||||
:root { --base-font-size: 14px; --ui-scale: 1.2; }
|
||||
}
|
||||
|
||||
@media (min-width: 2048px) {
|
||||
:root { --base-font-size: 16px; --ui-scale: 1.36; }
|
||||
:root { --base-font-size: 15px; --ui-scale: 1.36; }
|
||||
}
|
||||
|
||||
@media (min-width: 2560px) {
|
||||
:root { --base-font-size: 18px; --ui-scale: 1.58; }
|
||||
:root { --base-font-size: 16px; --ui-scale: 1.58; }
|
||||
}
|
||||
|
||||
@media (min-width: 3200px) {
|
||||
:root { --base-font-size: 20px; --ui-scale: 1.88; }
|
||||
:root { --base-font-size: 18px; --ui-scale: 1.88; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -1,52 +1,48 @@
|
||||
.box-window {
|
||||
width: min(760px, calc(100vw - 36px));
|
||||
width: min(860px, calc(100vw - 36px));
|
||||
height: min(560px, calc(100vh - 36px));
|
||||
zoom: var(--ui-scale);
|
||||
}
|
||||
|
||||
.box-toolbar {
|
||||
display: flex;
|
||||
body.fit-window .box-window {
|
||||
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;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: grid;
|
||||
grid-template-columns: 58px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
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;
|
||||
padding: 0 8px 6px;
|
||||
gap: 6px;
|
||||
color: #333333;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.box-address code {
|
||||
grid-column: 1;
|
||||
min-width: 0;
|
||||
height: 22px;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #000000;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #808080;
|
||||
@@ -54,6 +50,33 @@
|
||||
border-right: 1px solid #dfdfdf;
|
||||
border-bottom: 1px solid #dfdfdf;
|
||||
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 {
|
||||
@@ -169,6 +192,98 @@
|
||||
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 {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
@@ -205,6 +320,15 @@
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.box-command-row {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.box-address {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.box-panel {
|
||||
margin: 0 6px 8px;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,22 @@ body.fit-window .desktop-wrap {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-action[aria-disabled="true"] {
|
||||
color: #808080;
|
||||
text-shadow: 1px 1px 0 #ffffff;
|
||||
}
|
||||
|
||||
.menu-action[aria-disabled="true"] img {
|
||||
opacity: .55;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.menu-action[aria-disabled="true"]:hover,
|
||||
.menu-action[aria-disabled="true"]:focus-visible {
|
||||
color: #808080;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.menu-action img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -672,15 +688,21 @@ body.fit-window .desktop-wrap {
|
||||
}
|
||||
|
||||
.option-check input[type="checkbox"]:checked + span::after {
|
||||
content: "✓";
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: -3px;
|
||||
left: 4px;
|
||||
top: 6px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
color: #000000;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
font-weight: bold;
|
||||
background: #000000;
|
||||
box-shadow:
|
||||
2px 2px 0 #000000,
|
||||
4px 4px 0 #000000,
|
||||
6px 2px 0 #000000,
|
||||
8px 0 0 #000000,
|
||||
10px -2px 0 #000000;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.upload-select,
|
||||
@@ -753,7 +775,7 @@ body.fit-window .desktop-wrap {
|
||||
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
white-space: pre-wrap;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.terminal-box::after {
|
||||
@@ -871,6 +893,8 @@ body.fit-window .desktop-wrap {
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 90px);
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
@@ -884,17 +908,40 @@ body.fit-window .desktop-wrap {
|
||||
.popup-body ul,
|
||||
.popup-body ol { margin: 0 0 8px 18px; padding: 0; }
|
||||
.popup-body li { margin: 0 0 4px; }
|
||||
.popup-body pre {
|
||||
.popup-body .code-block {
|
||||
margin: 6px 0 10px;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
color: #00ff66;
|
||||
background: #000000;
|
||||
border: 0;
|
||||
font-family: 'PixelOperatorMono', 'Courier New', monospace;
|
||||
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
white-space: pre-wrap;
|
||||
white-space: pre;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.popup-window.is-about-popup .popup-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.about-popup-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.about-popup-content p:last-child {
|
||||
margin-top: auto;
|
||||
margin-bottom: 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
@@ -1012,16 +1059,16 @@ body.fit-window .desktop-wrap {
|
||||
.copy-fallback-text {
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
font-family: 'PixelOperatorMono', monospace;
|
||||
font-family: 'MonoCraft', 'PixelOperatorMono', monospace;
|
||||
}
|
||||
|
||||
.popup-body pre {
|
||||
.popup-body .code-block {
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
padding-bottom: 22px;
|
||||
}
|
||||
|
||||
.popup-body pre::after {
|
||||
.popup-body .code-block::after {
|
||||
content: "\A";
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
flex-direction: column;
|
||||
color: #000000;
|
||||
background-color: #c0c0c0;
|
||||
background-image:
|
||||
linear-gradient(180deg, rgba(255,255,255,.34), rgba(0,0,0,.06)),
|
||||
repeating-linear-gradient(45deg, rgba(255,255,255,.12) 0 1px, transparent 1px 5px);
|
||||
background-image: linear-gradient(180deg, rgba(255,255,255,.24), rgba(0,0,0,.06));
|
||||
border-top: 1px solid #ffffff;
|
||||
border-left: 1px solid #ffffff;
|
||||
border-right: 1px solid #000000;
|
||||
@@ -79,7 +77,7 @@
|
||||
border-right: 1px solid #000000;
|
||||
border-bottom: 1px solid #000000;
|
||||
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
160
static/js/app.js
160
static/js/app.js
@@ -171,17 +171,25 @@ function setStatus(message) {
|
||||
}
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
if (!el.toast) return;
|
||||
el.toast.textContent = message;
|
||||
el.toast.classList.remove("toast-info", "toast-warning", "toast-error", "is-visible");
|
||||
el.toast.classList.add(`toast-${type}`, "is-visible");
|
||||
clearTimeout(showToast.timer);
|
||||
showToast.timer = setTimeout(() => el.toast.classList.remove("is-visible"), 2600);
|
||||
window.WarpBoxUI.toast(message, type, { target: el.toast });
|
||||
}
|
||||
|
||||
function closeMenus() {
|
||||
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
|
||||
node.classList.remove("is-open");
|
||||
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
}
|
||||
|
||||
function disabledReasonFor(target) {
|
||||
const control = target.closest("[data-disabled-reason], button, input, select, textarea, .upload-dropzone");
|
||||
const control = target.closest("[data-disabled-reason], button, input, select, textarea, .upload-dropzone, .option-check, .option-row");
|
||||
if (!control) return "";
|
||||
if (control.classList.contains("option-check") || control.classList.contains("option-row")) {
|
||||
const nested = control.querySelector("input, select, textarea");
|
||||
if (nested?.disabled || nested?.readOnly || nested?.getAttribute("aria-disabled") === "true") {
|
||||
return nested.dataset.disabledReason || "This option is disabled right now.";
|
||||
}
|
||||
}
|
||||
if (control.classList.contains("upload-dropzone") && uploadLocked) {
|
||||
return control.dataset.disabledReason || "The current box is sealed after upload. Press Clear to start a new box.";
|
||||
}
|
||||
@@ -196,6 +204,7 @@ function announceDisabledReason(event) {
|
||||
if (!reason) return false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeMenus();
|
||||
showToast(reason, "warning");
|
||||
setStatus(reason);
|
||||
return true;
|
||||
@@ -225,8 +234,10 @@ function setShareUrl(url) {
|
||||
el.shareLink.title = shareUrl;
|
||||
el.shareLink.classList.toggle("is-empty", !shareUrl);
|
||||
el.shareLink.setAttribute("aria-disabled", shareUrl ? "false" : "true");
|
||||
el.copyButton.disabled = !shareUrl;
|
||||
el.copyButton.disabled = false;
|
||||
el.copyButton.setAttribute("aria-disabled", shareUrl ? "false" : "true");
|
||||
el.copyButton.dataset.disabledReason = shareUrl ? "" : "There is no share URL yet. Start an upload first.";
|
||||
updateDisabledReasons();
|
||||
updateTerminal();
|
||||
updateCurrentStep();
|
||||
}
|
||||
@@ -345,7 +356,8 @@ function createFileRow(item, index) {
|
||||
remove.textContent = "×";
|
||||
remove.dataset.remove = String(index);
|
||||
remove.title = uploadLocked ? "This file cannot be removed because this upload box was already created." : "Remove file";
|
||||
remove.disabled = uploadLocked;
|
||||
remove.disabled = false;
|
||||
remove.setAttribute("aria-disabled", uploadLocked ? "true" : "false");
|
||||
remove.dataset.disabledReason = uploadLocked ? "Files cannot be removed after the box is created. Press Clear to start another upload." : "";
|
||||
|
||||
const progress = document.createElement("span");
|
||||
@@ -762,7 +774,8 @@ function updateDisabledReasons() {
|
||||
else if (uploadLocked) reason = "This upload already started. Press Clear to create another box.";
|
||||
else if (hasQuotaError()) reason = "Over maximum upload size. Remove highlighted files or clear some files.";
|
||||
else if (!files.length) reason = "There are no files selected. Please select files to upload.";
|
||||
el.startButton.disabled = !uploadsEnabled || uploadLocked || hasQuotaError();
|
||||
el.startButton.disabled = false;
|
||||
el.startButton.setAttribute("aria-disabled", reason ? "true" : "false");
|
||||
el.startButton.dataset.disabledReason = reason;
|
||||
el.startButton.title = reason;
|
||||
}
|
||||
@@ -772,22 +785,31 @@ function updateDisabledReasons() {
|
||||
if (el.dropzone) {
|
||||
el.dropzone.dataset.disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : "");
|
||||
}
|
||||
document.querySelectorAll('[data-action="start-upload"]').forEach((button) => {
|
||||
const reason = el.startButton?.dataset.disabledReason || "";
|
||||
button.setAttribute("aria-disabled", reason ? "true" : "false");
|
||||
button.dataset.disabledReason = reason;
|
||||
});
|
||||
document.querySelectorAll('[data-action="browse"]').forEach((button) => {
|
||||
const reason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : "");
|
||||
button.setAttribute("aria-disabled", reason ? "true" : "false");
|
||||
button.dataset.disabledReason = reason;
|
||||
});
|
||||
document.querySelectorAll('[data-action="copy-link"]').forEach((button) => {
|
||||
button.setAttribute("aria-disabled", shareUrl ? "false" : "true");
|
||||
button.dataset.disabledReason = shareUrl ? "" : "There is no share URL yet. Start an upload first.";
|
||||
});
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const apiKey = el.apiKeyMode?.checked && validApiKey(el.apiKeyInput?.value || "") ? el.apiKeyInput.value.trim() : "";
|
||||
const settings = {
|
||||
expiry: el.expiry?.value || defaultRetention,
|
||||
password: el.password?.value || "",
|
||||
maxViews: el.maxViews?.value || "",
|
||||
boxName: el.boxName?.value || "",
|
||||
customSlug: el.customSlug?.value || "",
|
||||
downloadPage: Boolean(el.downloadPage?.checked),
|
||||
allowZip: Boolean(el.allowZip?.checked),
|
||||
allowPreview: Boolean(el.allowPreview?.checked),
|
||||
keepFilenames: Boolean(el.keepFilenames?.checked),
|
||||
privateBox: Boolean(el.privateBox?.checked),
|
||||
apiKeyMode: Boolean(el.apiKeyMode?.checked),
|
||||
apiKey: el.apiKeyInput?.value || "",
|
||||
apiKey,
|
||||
};
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||
}
|
||||
@@ -797,25 +819,19 @@ function loadSettings() {
|
||||
try {
|
||||
settings = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}");
|
||||
} catch (_) {}
|
||||
if (settings.expiry && Array.from(el.expiry?.options || []).some((option) => option.value === settings.expiry)) el.expiry.value = settings.expiry;
|
||||
if (el.password) el.password.value = settings.password || "";
|
||||
if (el.maxViews) el.maxViews.value = settings.maxViews || "";
|
||||
if (el.boxName) el.boxName.value = settings.boxName || "";
|
||||
if (el.customSlug) el.customSlug.value = settings.customSlug || "";
|
||||
if (el.downloadPage) el.downloadPage.checked = settings.downloadPage !== false;
|
||||
if (el.allowZip) el.allowZip.checked = settings.allowZip !== false;
|
||||
if (el.allowPreview) el.allowPreview.checked = settings.allowPreview !== false;
|
||||
if (el.keepFilenames) el.keepFilenames.checked = settings.keepFilenames !== false;
|
||||
if (el.privateBox) el.privateBox.checked = Boolean(settings.privateBox);
|
||||
if (el.apiKeyMode) el.apiKeyMode.checked = Boolean(settings.apiKeyMode);
|
||||
if (el.apiKeyInput) el.apiKeyInput.value = settings.apiKey || "";
|
||||
if (el.apiKeyInput) el.apiKeyInput.value = validApiKey(settings.apiKey || "") ? settings.apiKey : "";
|
||||
syncZipForRetention();
|
||||
syncApiKeyField();
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncMenuChecks() {
|
||||
const downloadCheck = document.querySelector("[data-download-page-check]");
|
||||
if (downloadCheck) downloadCheck.textContent = el.downloadPage?.checked ? "✓" : "";
|
||||
updateDisabledReasons();
|
||||
}
|
||||
|
||||
function syncApiKeyField() {
|
||||
@@ -841,6 +857,7 @@ function validateApiKeyField() {
|
||||
const value = el.apiKeyInput.value.trim();
|
||||
if (!value) {
|
||||
el.apiKeyState.textContent = "waiting";
|
||||
saveSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -850,11 +867,22 @@ function validateApiKeyField() {
|
||||
apiKeyTimer = setTimeout(() => {
|
||||
wrapper?.classList.remove("is-checking");
|
||||
el.apiKeyInput.disabled = uploadLocked;
|
||||
el.apiKeyState.textContent = value.length >= 12 ? "saved locally" : "too short";
|
||||
if (value.length < 12) showToast("API key looks too short. It was saved locally, but not sent during browser uploads.", "warning");
|
||||
if (validApiKey(value)) {
|
||||
el.apiKeyState.textContent = "saved locally";
|
||||
saveSettings();
|
||||
} else {
|
||||
el.apiKeyInput.value = "";
|
||||
el.apiKeyState.textContent = "invalid";
|
||||
saveSettings();
|
||||
showToast("Invalid API key removed. Paste a valid API key to save it.", "warning");
|
||||
}
|
||||
}, 650);
|
||||
}
|
||||
|
||||
function validApiKey(value) {
|
||||
return /^[A-Za-z0-9._-]{12,}$/.test(String(value || "").trim());
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
@@ -966,17 +994,17 @@ function showWarningDialog(title, message) {
|
||||
}
|
||||
|
||||
function openPopup(title, html, about = false) {
|
||||
if (!el.docPopup || !el.docPopupTitle || !el.docPopupBody) return;
|
||||
el.docPopupTitle.textContent = title;
|
||||
el.docPopupBody.innerHTML = html;
|
||||
el.docPopup.classList.toggle("is-about-popup", about);
|
||||
el.docPopup.classList.add("is-visible");
|
||||
el.modalBackdrop?.classList.add("is-visible");
|
||||
window.WarpBoxUI.openPopup(title, html, {
|
||||
about,
|
||||
popup: el.docPopup,
|
||||
title: el.docPopupTitle,
|
||||
body: el.docPopupBody,
|
||||
backdrop: el.modalBackdrop,
|
||||
});
|
||||
}
|
||||
|
||||
function closeDoc() {
|
||||
el.docPopup?.classList.remove("is-visible", "is-about-popup");
|
||||
el.modalBackdrop?.classList.remove("is-visible");
|
||||
window.WarpBoxUI.closePopup({ popup: el.docPopup, backdrop: el.modalBackdrop });
|
||||
}
|
||||
|
||||
async function showTemplatePopup(title, templateName, data = {}, about = false) {
|
||||
@@ -1018,10 +1046,7 @@ document.addEventListener("click", (event) => {
|
||||
if (menuButton) {
|
||||
const item = menuButton.closest(".menu-item");
|
||||
const isOpen = item.classList.contains("is-open");
|
||||
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
|
||||
node.classList.remove("is-open");
|
||||
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
closeMenus();
|
||||
item.classList.toggle("is-open", !isOpen);
|
||||
menuButton.setAttribute("aria-expanded", String(!isOpen));
|
||||
return;
|
||||
@@ -1029,7 +1054,7 @@ document.addEventListener("click", (event) => {
|
||||
|
||||
const action = event.target.closest("[data-action]")?.dataset.action;
|
||||
if (action) {
|
||||
document.querySelectorAll(".menu-item.is-open").forEach((node) => node.classList.remove("is-open"));
|
||||
closeMenus();
|
||||
if (action === "browse") el.fileInput?.click();
|
||||
if (action === "start-upload") startUpload();
|
||||
if (action === "copy-link") copyText("Share URL", shareUrl, shareUrl);
|
||||
@@ -1054,7 +1079,6 @@ document.addEventListener("click", (event) => {
|
||||
syncMenuChecks();
|
||||
}
|
||||
if (action === "help" || action === "side-help") openDoc("faq");
|
||||
if (action === "terminal-help") el.terminal?.focus();
|
||||
if (action === "coming-soon") showToast("Coming Soon, not implemented just yet.");
|
||||
if (action === "fake-close") showToast("Close button denied. The upload window is staying open.", "warning");
|
||||
if (action === "minimize") showToast("Minimize requested. WarpBox stays visible so your queue is safe.");
|
||||
@@ -1093,10 +1117,7 @@ document.addEventListener("click", (event) => {
|
||||
if (event.target.id === "confirm-clear-no" || event.target.id === "fallback-close") closeDoc();
|
||||
|
||||
if (!event.target.closest(".menu-item")) {
|
||||
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
|
||||
node.classList.remove("is-open");
|
||||
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
closeMenus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1107,10 +1128,7 @@ document.addEventListener("mousedown", (event) => {
|
||||
document.querySelectorAll(".menu-item").forEach((item) => {
|
||||
item.addEventListener("mouseenter", () => {
|
||||
if (!document.querySelector(".menu-item.is-open")) return;
|
||||
document.querySelectorAll(".menu-item.is-open").forEach((node) => {
|
||||
node.classList.remove("is-open");
|
||||
node.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
closeMenus();
|
||||
item.classList.add("is-open");
|
||||
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
@@ -1148,6 +1166,46 @@ el.copyCurlButton?.addEventListener("click", () => copyText("cURL command", getC
|
||||
el.docPopupClose?.addEventListener("click", closeDoc);
|
||||
el.modalBackdrop?.addEventListener("click", closeDoc);
|
||||
|
||||
el.maxViews?.addEventListener("wheel", (event) => {
|
||||
if (el.maxViews.disabled || el.maxViews.readOnly) return;
|
||||
event.preventDefault();
|
||||
const delta = event.deltaY < 0 ? 1 : -1;
|
||||
const modifier = event.ctrlKey && event.shiftKey ? 50 : event.shiftKey ? 15 : event.ctrlKey ? 5 : 1;
|
||||
const min = Number.parseInt(el.maxViews.min || "1", 10);
|
||||
const max = Number.parseInt(el.maxViews.max || "9999", 10);
|
||||
const current = Number.parseInt(el.maxViews.value || String(min), 10);
|
||||
el.maxViews.value = String(Math.max(min, Math.min(max, current + (delta * modifier))));
|
||||
saveSettings();
|
||||
updateTerminal();
|
||||
});
|
||||
|
||||
el.apiKeyInput?.addEventListener("keydown", (event) => {
|
||||
const allowed = event.ctrlKey || event.metaKey || event.altKey || [
|
||||
"Tab",
|
||||
"Shift",
|
||||
"Control",
|
||||
"Alt",
|
||||
"Meta",
|
||||
"Escape",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"Home",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
].includes(event.key);
|
||||
if (allowed) return;
|
||||
event.preventDefault();
|
||||
showToast("Only pasting the API key is supported.", "warning");
|
||||
setStatus("Only pasting the API key is supported");
|
||||
});
|
||||
|
||||
el.apiKeyInput?.addEventListener("paste", () => {
|
||||
setTimeout(validateApiKeyField, 0);
|
||||
});
|
||||
|
||||
[el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.allowZip, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean).forEach((control) => {
|
||||
control.addEventListener("input", () => {
|
||||
if (control === el.boxName) syncSlugFromName();
|
||||
@@ -1172,7 +1230,7 @@ el.modalBackdrop?.addEventListener("click", closeDoc);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeDoc();
|
||||
document.querySelectorAll(".menu-item.is-open").forEach((node) => node.classList.remove("is-open"));
|
||||
closeMenus();
|
||||
}
|
||||
if (event.key === "F1") {
|
||||
event.preventDefault();
|
||||
|
||||
274
static/js/box.js
274
static/js/box.js
@@ -1,20 +1,182 @@
|
||||
const boxPanel = document.querySelector(".box-panel[data-box-id]");
|
||||
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";
|
||||
|
||||
document.querySelectorAll('.box-file[aria-disabled="true"]').forEach((item) => {
|
||||
item.addEventListener("click", (event) => {
|
||||
if (item.getAttribute("aria-disabled") === "true") {
|
||||
event.preventDefault();
|
||||
}
|
||||
let contextFile = null;
|
||||
|
||||
function htmlEscape(value) {
|
||||
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) {
|
||||
const item = document.querySelector(`.box-file[data-file-id="${file.id}"]`);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (!item) return;
|
||||
|
||||
const meta = item.querySelector(".box-file-meta");
|
||||
const icon = item.querySelector(".box-file-icon");
|
||||
@@ -26,6 +188,11 @@ function updateBoxFile(file) {
|
||||
item.classList.toggle("is-loading", !isComplete && !isFailed);
|
||||
item.classList.toggle("has-thumbnail", Boolean(file.thumbnail_path));
|
||||
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;
|
||||
|
||||
if (isComplete && !zipOnly) {
|
||||
@@ -38,27 +205,22 @@ function updateBoxFile(file) {
|
||||
item.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
|
||||
if (meta) {
|
||||
meta.textContent = `${file.status_label} · ${file.size_label}`;
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
icon.src = file.thumbnail_path || file.icon_path;
|
||||
}
|
||||
if (meta) meta.textContent = `${file.status_label} · ${file.size_label}`;
|
||||
if (icon) icon.src = file.thumbnail_path || file.icon_path;
|
||||
}
|
||||
|
||||
async function refreshBoxStatus() {
|
||||
if (!boxPanel) {
|
||||
return false;
|
||||
}
|
||||
if (!boxPanel) return false;
|
||||
|
||||
const boxID = boxPanel.dataset.boxId;
|
||||
const response = await fetch(`/box/${boxID}/status`);
|
||||
if (!response.ok) {
|
||||
return true;
|
||||
}
|
||||
if (!response.ok) return true;
|
||||
|
||||
const result = await response.json();
|
||||
if (boxExpiryMeta && typeof result.expires_at === "string") {
|
||||
boxExpiryMeta.dataset.expiresAt = result.expires_at;
|
||||
updateExpiryCountdown();
|
||||
}
|
||||
result.files.forEach(updateBoxFile);
|
||||
|
||||
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) {
|
||||
const pollMS = Number.parseInt(boxPanel.dataset.pollMs, 10) || 5000;
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const hasLoadingFiles = await refreshBoxStatus();
|
||||
if (!hasLoadingFiles) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
} catch (error) {
|
||||
// Keep polling through temporary network/server hiccups; otherwise
|
||||
// an in-progress file can appear stuck forever after one bad poll.
|
||||
if (!hasLoadingFiles) clearInterval(timer);
|
||||
} catch (_) {
|
||||
// Keep polling through temporary network/server hiccups.
|
||||
}
|
||||
}, pollMS);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
window.WBUtils = (() => {
|
||||
function renderTemplate(template, data = {}) {
|
||||
return String(template).replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
|
||||
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]) : "";
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
})();
|
||||
@@ -1,3 +1,6 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
<h3>Upload with cURL</h3>
|
||||
<p>WarpBox accepts normal multipart form uploads through the compatibility endpoint:</p>
|
||||
<pre>curl \
|
||||
<code class="code-block">curl \
|
||||
-F 'files=@./my-file.zip' \
|
||||
-F 'retention=1h' \
|
||||
{{ origin }}/upload
|
||||
</pre>
|
||||
</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>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<h3>Upload examples</h3>
|
||||
<h4>Basic CLI upload</h4>
|
||||
<pre>curl \
|
||||
<code class="code-block">curl \
|
||||
-F 'files=@./photo.png' \
|
||||
-F 'retention=24h' \
|
||||
{{ origin }}/upload
|
||||
</pre>
|
||||
</code>
|
||||
<h4>Multiple files with password</h4>
|
||||
<pre>curl \
|
||||
<code class="code-block">curl \
|
||||
-F 'files=@./one.png' \
|
||||
-F 'files=@./two.zip' \
|
||||
-F 'retention=1h' \
|
||||
-F 'password=secret-pass' \
|
||||
{{ origin }}/upload
|
||||
</pre>
|
||||
</code>
|
||||
<h4>Go</h4>
|
||||
<pre>package main
|
||||
<code class="code-block">package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -48,9 +48,9 @@ func main() {
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
</pre>
|
||||
</code>
|
||||
<h4>Java 11+ HttpClient</h4>
|
||||
<pre>import java.net.URI;
|
||||
<code class="code-block">import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
@@ -84,9 +84,9 @@ public class UploadWarpBox {
|
||||
System.out.println(response.body());
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</code>
|
||||
<h4>JavaScript Node.js</h4>
|
||||
<pre>import { openAsBlob } from 'node:fs';
|
||||
<code class="code-block">import { openAsBlob } from 'node:fs';
|
||||
|
||||
const file = await openAsBlob('./photo.png');
|
||||
const form = new FormData();
|
||||
@@ -99,4 +99,4 @@ const res = await fetch('{{ origin }}/upload', {
|
||||
});
|
||||
|
||||
console.log(await res.text());
|
||||
</pre>
|
||||
</code>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WarpBox - {{ .BoxID }}</title>
|
||||
<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/window.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">
|
||||
<h1 id="box-window-title">WarpBox Explorer - {{ .BoxID }}</h1>
|
||||
</div>
|
||||
<div class="win98-window-controls" aria-hidden="true">
|
||||
<span class="win98-control">_</span>
|
||||
<span class="win98-control">□</span>
|
||||
<span class="win98-control">×</span>
|
||||
<div class="win98-window-controls" aria-label="Window controls">
|
||||
<button class="win98-control win98-minimize" type="button" data-action="minimize" title="Minimize" aria-label="Minimize">_</button>
|
||||
<button class="win98-control" type="button" data-action="toggle-fit" title="Fit window" aria-label="Maximize">□</button>
|
||||
<button class="win98-control" type="button" data-action="fake-close" title="Close" aria-label="Close">×</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="win98-menu box-menu" aria-hidden="true">
|
||||
<span class="win98-menu-option">File</span>
|
||||
<span class="win98-menu-option">Edit</span>
|
||||
<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>
|
||||
<div class="box-command-row">
|
||||
<button class="box-address" type="button" id="box-address" data-copy-url="{{ .BoxID }}" title="Copy current page URL">{{ .BoxID }}</button>
|
||||
<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>
|
||||
{{ 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 }}
|
||||
</div>
|
||||
|
||||
<div class="box-address">
|
||||
<span>Address</span>
|
||||
<code>/box/{{ .BoxID }}</code>
|
||||
</div>
|
||||
|
||||
{{ if .RetentionLabel }}
|
||||
<div class="box-meta">
|
||||
<span>Retention</span>
|
||||
<span>{{ .RetentionLabel }}</span>
|
||||
<div class="box-meta" data-expires-at="{{ .ExpiresAtISO }}">
|
||||
<span id="box-expiry-text">Expires in {{ .RetentionLabel }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -57,7 +43,7 @@
|
||||
{{ if .Files }}
|
||||
<div class="box-file-grid" aria-label="Uploaded 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">
|
||||
<span class="box-file-name">{{ .Name }}</span>
|
||||
<span class="box-file-meta">{{ .StatusLabel }} · {{ .SizeLabel }}</span>
|
||||
@@ -75,6 +61,24 @@
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
<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>
|
||||
<button class="menu-action" type="button" data-action="copy-link"><img src="/static/img/sprites/frame_web-0.png" alt="" aria-hidden="true"><span>Copy share URL</span><span class="shortcut">Ctrl+L</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>
|
||||
@@ -41,7 +40,7 @@
|
||||
<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"><img src="/static/img/sprites/frame_web-0.png" alt="" aria-hidden="true"><span>Copy share URL</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>
|
||||
@@ -51,14 +50,12 @@
|
||||
<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>
|
||||
<button class="menu-action" type="button" data-action="toggle-page"><img src="/static/img/sprites/frame_web-0.png" alt="" aria-hidden="true"><span>Download page</span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div 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-action="terminal-help"><img src="/static/img/icons/shell_window1.png" alt="" aria-hidden="true"><span>Show cURL command</span><span></span></button>
|
||||
<button class="menu-action" type="button" data-doc="about"><img src="/static/WarpBoxLogo.png" alt="" aria-hidden="true"><span>About WarpBox</span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +96,7 @@
|
||||
<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" disabled data-disabled-reason="There is no share URL yet. Start an upload first.">Copy</button>
|
||||
<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>
|
||||
|
||||
@@ -110,7 +107,7 @@
|
||||
|
||||
<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" {{ if not .UploadsEnabled }}disabled{{ end }} data-disabled-reason="Start upload is unavailable right now.">Start upload</button>
|
||||
<button class="win98-button start-upload-cta" type="submit" id="start-button" tabindex="4" aria-disabled="true" data-disabled-reason="There are no files selected. Please select files to upload.">Start upload</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -145,7 +142,7 @@
|
||||
</label>
|
||||
<label class="option-row">
|
||||
<span>Max views:</span>
|
||||
<input class="upload-text-input" id="max-views" type="number" min="1" max="9999" placeholder="local note">
|
||||
<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>
|
||||
@@ -238,6 +235,7 @@
|
||||
</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>
|
||||
|
||||
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