refactor(code): Cleaned-up the code base
This commit is contained in:
117
static/css/components/buttons.css
Normal file
117
static/css/components/buttons.css
Normal file
@@ -0,0 +1,117 @@
|
||||
.menu-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 24px;
|
||||
padding: 1px 6px;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
height: 20px;
|
||||
min-width: 54px;
|
||||
padding: 0 8px;
|
||||
color: #000000;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-button:hover,
|
||||
.menu-button:focus-visible {
|
||||
border-top: 1px solid #ffffff;
|
||||
border-left: 1px solid #ffffff;
|
||||
border-right: 1px solid #808080;
|
||||
border-bottom: 1px solid #808080;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menu-popup {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 0;
|
||||
min-width: 198px;
|
||||
padding: 2px;
|
||||
display: none;
|
||||
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: 20;
|
||||
}
|
||||
|
||||
.menu-item.is-open .menu-popup {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu-action {
|
||||
width: 100%;
|
||||
min-height: 22px;
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
color: #000000;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
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;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.menu-action:hover,
|
||||
.menu-action:focus-visible {
|
||||
color: #ffffff;
|
||||
background: #000078;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menu-separator {
|
||||
height: 1px;
|
||||
margin: 3px 2px;
|
||||
background: #808080;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.menu-action:hover .shortcut {
|
||||
color: #ffffff;
|
||||
}
|
||||
38
static/css/components/toast.css
Normal file
38
static/css/components/toast.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.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: 60;
|
||||
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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
36
static/css/upload/actions.css
Normal file
36
static/css/upload/actions.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.upload-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.start-upload-cta {
|
||||
min-width: 128px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
isolation: isolate;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.start-upload-cta.is-current-step {
|
||||
animation: start-ready-rainbow-breathe 1150ms ease-in-out infinite;
|
||||
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 0 0 0 1px #000000;
|
||||
}
|
||||
|
||||
.start-upload-cta.is-current-step::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
padding: 4px;
|
||||
background: linear-gradient(90deg, #ff004c, #ffcc00, #00d26a, #00a2ff, #8c48ff, #ff004c, #ffcc00);
|
||||
background-size: 280% 100%;
|
||||
opacity: .9;
|
||||
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
animation: start-border-rainbow-slide 1850ms linear infinite;
|
||||
}
|
||||
101
static/css/upload/dialog-content.css
Normal file
101
static/css/upload/dialog-content.css
Normal file
@@ -0,0 +1,101 @@
|
||||
.duplicate-list,
|
||||
.quota-dialog-list {
|
||||
margin: 8px 0;
|
||||
padding: 6px 6px 6px 28px;
|
||||
background: #ffffff;
|
||||
border-top: 2px solid #808080;
|
||||
border-left: 2px solid #808080;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.quota-dialog-summary,
|
||||
.quota-note {
|
||||
padding: 8px;
|
||||
background: #ffffcc;
|
||||
border: 1px solid #808080;
|
||||
}
|
||||
|
||||
.quota-meter-list,
|
||||
.faq-list,
|
||||
.shortcut-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.quota-meter,
|
||||
.faq-item,
|
||||
.shortcut-list li {
|
||||
padding: 8px;
|
||||
background: #dfdfdf;
|
||||
border-top: 1px solid #ffffff;
|
||||
border-left: 1px solid #ffffff;
|
||||
border-right: 1px solid #808080;
|
||||
border-bottom: 1px solid #808080;
|
||||
}
|
||||
|
||||
.quota-meter-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quota-meter-track {
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border-top: 2px solid #808080;
|
||||
border-left: 2px solid #808080;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.quota-meter-bar {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #000078;
|
||||
}
|
||||
|
||||
.copy-fallback-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.copy-fallback-text {
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
font-family: 'MonoCraft', 'PixelOperatorMono', monospace;
|
||||
}
|
||||
|
||||
.popup-body .code-block {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.popup-body .code-block code {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
white-space: inherit;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.kbd {
|
||||
display: inline-block;
|
||||
min-width: 18px;
|
||||
padding: 1px 5px;
|
||||
color: #000000;
|
||||
background: #c0c0c0;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: inset 1px 1px 0 #ffffff, inset -1px -1px 0 #808080;
|
||||
text-align: center;
|
||||
}
|
||||
95
static/css/upload/dialogs.css
Normal file
95
static/css/upload/dialogs.css
Normal file
@@ -0,0 +1,95 @@
|
||||
.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-window.is-about-popup {
|
||||
width: min(360px, calc(100vw - 28px));
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 90px);
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
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;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 8px;
|
||||
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;
|
||||
-webkit-user-select: text;
|
||||
box-sizing: border-box;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.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 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
41
static/css/upload/folders.css
Normal file
41
static/css/upload/folders.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.folder-icon-button {
|
||||
flex: 0 0 86px;
|
||||
width: 86px;
|
||||
min-width: 86px;
|
||||
height: 68px;
|
||||
display: grid;
|
||||
grid-template-rows: 34px 1fr;
|
||||
place-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
color: #000000;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.folder-icon-button img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.folder-icon-button:hover,
|
||||
.folder-icon-button:focus-visible {
|
||||
color: #ffffff;
|
||||
background: #000078;
|
||||
border: 1px dotted #ffffff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.folder-icon-button-disabled {
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
.folder-icon-button-disabled img {
|
||||
filter: grayscale(.9);
|
||||
opacity: .75;
|
||||
}
|
||||
43
static/css/upload/layout.css
Normal file
43
static/css/upload/layout.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.upload-main {
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-wrap {
|
||||
--window-height: 736px;
|
||||
--side-width: 440px;
|
||||
width: min(1278px, 100%);
|
||||
height: min(var(--window-height), calc(100vh - 36px));
|
||||
max-height: calc(100vh - 36px);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 820px) var(--side-width);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
overflow: hidden;
|
||||
zoom: var(--ui-scale);
|
||||
}
|
||||
|
||||
body.fit-window .desktop-wrap {
|
||||
width: min(100%, calc(100vw / var(--ui-scale) - 20px));
|
||||
height: min(calc(100vh / var(--ui-scale) - 20px), 900px);
|
||||
max-height: none;
|
||||
grid-template-columns: minmax(0, 1fr) var(--side-width);
|
||||
}
|
||||
|
||||
.upload-window {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-form {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
148
static/css/upload/options.css
Normal file
148
static/css/upload/options.css
Normal file
@@ -0,0 +1,148 @@
|
||||
.box-options-form {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-height: 100%;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.box-options-form.is-locked {
|
||||
opacity: .82;
|
||||
filter: grayscale(.12);
|
||||
}
|
||||
|
||||
.box-options-form.is-locked::after {
|
||||
content: "Box sealed after upload";
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
padding: 5px 6px;
|
||||
color: #000000;
|
||||
background: #dfdfdf;
|
||||
border-top: 1px solid #808080;
|
||||
border-left: 1px solid #808080;
|
||||
border-right: 1px solid #ffffff;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.option-row {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option-check {
|
||||
position: relative;
|
||||
min-height: 18px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option-check input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.option-check span {
|
||||
position: relative;
|
||||
min-height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.option-check span::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #ffffff;
|
||||
border-top: 2px solid #808080;
|
||||
border-left: 2px solid #808080;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
box-shadow: inset -1px -1px 0 #dfdfdf;
|
||||
}
|
||||
|
||||
.option-check input[type="checkbox"]:checked + span::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 6px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
color: #000000;
|
||||
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,
|
||||
.upload-text-input {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
padding: 1px 4px;
|
||||
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-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.upload-text-input:disabled,
|
||||
.upload-select:disabled,
|
||||
.box-options-form.is-locked input[readonly],
|
||||
.box-options-form.is-locked input:disabled,
|
||||
.box-options-form.is-locked select:disabled {
|
||||
color: #404040;
|
||||
background: repeating-linear-gradient(45deg, #d0d0d0 0 4px, #c7c7c7 4px 8px);
|
||||
}
|
||||
|
||||
.api-key-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.api-key-row.is-visible {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.api-key-field {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.api-key-state {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 3px;
|
||||
color: #000078;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.api-key-field.is-checking::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
background: repeating-linear-gradient(90deg, rgba(0,0,120,.16) 0 8px, rgba(15,128,205,.16) 8px 16px);
|
||||
animation: api-key-scan 700ms steps(6, end) infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
41
static/css/upload/panel.css
Normal file
41
static/css/upload/panel.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.upload-panel {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
margin: 0 8px 8px;
|
||||
padding: 12px;
|
||||
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);
|
||||
}
|
||||
|
||||
.upload-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 270px;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
color: #000000;
|
||||
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;
|
||||
}
|
||||
|
||||
.upload-heading {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
line-height: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.upload-subtext {
|
||||
margin: 0;
|
||||
color: #333333;
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
}
|
||||
323
static/css/upload/queue.css
Normal file
323
static/css/upload/queue.css
Normal file
@@ -0,0 +1,323 @@
|
||||
.upload-quota {
|
||||
min-width: 250px;
|
||||
padding: 7px;
|
||||
overflow: hidden;
|
||||
background: #c7d8f2;
|
||||
border-top: 1px solid #ffffff;
|
||||
border-left: 1px solid #ffffff;
|
||||
border-right: 1px solid #404040;
|
||||
border-bottom: 1px solid #404040;
|
||||
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #e9f2ff;
|
||||
font-size: 12px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.upload-quota strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.upload-quota.is-quota-warning {
|
||||
background: repeating-linear-gradient(45deg, #ffdede 0 5px, #fff2a8 5px 10px);
|
||||
border-color: #800000;
|
||||
animation: quota-warning-breathe 900ms steps(4, end) infinite;
|
||||
}
|
||||
|
||||
.upload-quota-track,
|
||||
.upload-overall-track,
|
||||
.upload-progress {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
background-image: repeating-linear-gradient(to right, rgba(0,0,0,.05) 0 1px, transparent 1px 18px);
|
||||
border-top: 2px solid #808080;
|
||||
border-left: 2px solid #808080;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.upload-quota-track {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.upload-quota-bar,
|
||||
.upload-overall-bar,
|
||||
.upload-progress-bar {
|
||||
display: block;
|
||||
width: 0%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000078;
|
||||
background-image: repeating-linear-gradient(to right, rgba(255,255,255,.12) 0 1px, transparent 1px 18px);
|
||||
transform-origin: left center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.upload-quota-bar.is-over-quota {
|
||||
background-image: repeating-linear-gradient(45deg, #800000 0 7px, #ffcc00 7px 14px);
|
||||
}
|
||||
|
||||
.upload-dropzone {
|
||||
flex: 0 0 auto;
|
||||
min-height: 154px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
background: repeating-linear-gradient(45deg, #dfdfdf 0 4px, #e9e9e9 4px 8px), #dfdfdf;
|
||||
border: 1px solid #808080;
|
||||
box-shadow: inset 1px 1px 0 #ffffff, inset -1px -1px 0 #808080, inset 2px 2px 0 rgba(0,0,0,.18), 0 1px 0 rgba(255,255,255,.7);
|
||||
}
|
||||
|
||||
.upload-dropzone.is-dragging,
|
||||
.upload-dropzone:hover {
|
||||
background: repeating-linear-gradient(45deg, #c7d8f2 0 4px, #d8e5f8 4px 8px), #c7d8f2;
|
||||
outline: 2px dashed #000078;
|
||||
outline-offset: -6px;
|
||||
}
|
||||
|
||||
.upload-dropzone.is-current-step {
|
||||
animation: dropzone-attention 1500ms steps(5, end) infinite;
|
||||
}
|
||||
|
||||
.upload-dropzone.is-locked {
|
||||
opacity: .72;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(.3);
|
||||
}
|
||||
|
||||
.upload-icon-img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.upload-primary {
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.upload-secondary {
|
||||
color: #333333;
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.upload-linklike {
|
||||
color: #000078;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.upload-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.upload-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
margin-top: 12px;
|
||||
padding: 5px 8px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #808080;
|
||||
border-left: 1px solid #808080;
|
||||
border-right: 1px solid #dfdfdf;
|
||||
border-bottom: 1px solid #dfdfdf;
|
||||
box-shadow: inset 1px 1px 0 rgba(0,0,0,.16), inset -1px -1px 0 rgba(255,255,255,.75);
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.upload-detail-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.upload-file-count {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.upload-file-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
margin-top: 8px;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
border-top: 2px solid #606060;
|
||||
border-left: 2px solid #606060;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.upload-empty-state {
|
||||
margin: 0;
|
||||
padding: 10px 8px;
|
||||
color: #555555;
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.upload-file-row {
|
||||
display: grid;
|
||||
grid-template-columns: 22px minmax(0, 1fr) 82px 30px;
|
||||
grid-template-rows: 20px 8px;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #dfdfdf;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
column-gap: 6px;
|
||||
}
|
||||
|
||||
.upload-file-row:nth-child(odd) { background: rgba(255,255,255,.92); }
|
||||
.upload-file-row:nth-child(even) { background: rgba(240,244,255,.88); }
|
||||
.upload-file-row:hover { background: #d8e5f8; }
|
||||
.upload-file-row.is-working { animation: upload-row-loading 900ms steps(2, end) infinite; }
|
||||
.upload-file-row.is-failed { background: #ffe2e2 !important; }
|
||||
.upload-file-row.is-too-large { position: relative; background: #fff0b8 !important; animation: row-warning-breathe 900ms steps(4, end) infinite; }
|
||||
|
||||
.upload-file-row.is-too-large::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
pointer-events: none;
|
||||
border: 2px solid transparent;
|
||||
border-image: repeating-linear-gradient(90deg, #800000 0 8px, #ffcc00 8px 16px) 1;
|
||||
}
|
||||
|
||||
.upload-file-icon {
|
||||
grid-row: 1 / 3;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.upload-file-row.has-thumbnail .upload-file-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: cover;
|
||||
background: #ffffff;
|
||||
border: 1px solid #808080;
|
||||
}
|
||||
|
||||
.upload-file-name,
|
||||
.upload-file-size {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.upload-file-size {
|
||||
text-align: right;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.upload-file-remove {
|
||||
grid-column: 4;
|
||||
grid-row: 1 / 3;
|
||||
justify-self: end;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 2;
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.upload-file-row.is-uploaded .upload-progress-bar { background-color: #008000; }
|
||||
.upload-file-row.is-failed .upload-progress-bar { width: 100%; background-color: #800000; }
|
||||
|
||||
.upload-progress-bar.just-completed,
|
||||
.upload-overall-bar.just-completed {
|
||||
animation: progress-impact-bar 520ms steps(5, end) 1;
|
||||
}
|
||||
|
||||
.upload-progress-bar.just-completed::after,
|
||||
.upload-overall-bar.just-completed::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
top: 50%;
|
||||
width: 12px;
|
||||
height: 22px;
|
||||
transform: translateY(-50%);
|
||||
background: repeating-linear-gradient(45deg, rgba(255,255,255,.95) 0 2px, rgba(0,255,102,.85) 2px 4px, transparent 4px 6px);
|
||||
box-shadow: 0 0 0 1px #ffffff, 0 0 8px #00ff66;
|
||||
pointer-events: none;
|
||||
animation: progress-impact-spark 520ms steps(5, end) 1;
|
||||
}
|
||||
|
||||
.upload-result {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr) 72px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 36px;
|
||||
margin-top: 8px;
|
||||
padding: 4px 6px;
|
||||
background: #dfdfdf;
|
||||
border-top: 1px solid #808080;
|
||||
border-left: 1px solid #808080;
|
||||
border-right: 1px solid #ffffff;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
box-shadow: inset 1px 1px 0 rgba(0,0,0,.16), inset -1px -1px 0 rgba(255,255,255,.75);
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.upload-result.is-current-step {
|
||||
animation: share-ready-pulse 1100ms steps(4, end) infinite;
|
||||
}
|
||||
|
||||
.upload-result-label { font-weight: bold; }
|
||||
.upload-result-link { min-width: 0; overflow: hidden; color: #000078; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.upload-result-link.is-empty { color: #555555; text-decoration: none; pointer-events: none; }
|
||||
.upload-share-button { min-width: 72px; width: 72px; height: 24px; font-size: 12px; line-height: 12px; }
|
||||
|
||||
.upload-overall {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 42px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 28px;
|
||||
padding: 0 8px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.upload-overall-track {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.upload-overall-percent {
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
}
|
||||
123
static/css/upload/responsive.css
Normal file
123
static/css/upload/responsive.css
Normal file
@@ -0,0 +1,123 @@
|
||||
@keyframes upload-row-loading { 0% { background-color: #ffffff; } 100% { background-color: #e6e6e6; } }
|
||||
@keyframes quota-warning-breathe { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.08); } }
|
||||
@keyframes row-warning-breathe { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.12); } }
|
||||
@keyframes dropzone-attention { 0%, 100% { filter: brightness(1); transform: translateY(0); } 50% { filter: brightness(1.07); transform: translateY(-1px); } }
|
||||
@keyframes share-ready-pulse { 50% { filter: brightness(1.08); box-shadow: 0 0 0 2px #000078; } }
|
||||
@keyframes start-ready-rainbow-breathe { 0%, 100% { transform: rotate(-.35deg) scale(1); } 50% { transform: rotate(.35deg) scale(1.016); } }
|
||||
@keyframes start-border-rainbow-slide { from { background-position: 0% 50%; } to { background-position: 100% 50%; } }
|
||||
@keyframes progress-impact-bar { 0% { filter: brightness(1); } 35% { filter: brightness(1.75); } 100% { filter: brightness(1); } }
|
||||
@keyframes progress-impact-spark { 0% { opacity: 0; transform: translateY(-50%) scale(.7); } 30% { opacity: 1; transform: translateY(-50%) scale(1.18); } 100% { opacity: 0; transform: translateY(-50%) scale(.7); } }
|
||||
@keyframes terminal-cursor { 50% { opacity: 0; } }
|
||||
@keyframes 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; } }
|
||||
@keyframes api-key-scan { to { background-position: 32px 0; } }
|
||||
|
||||
@media (max-width: 1320px) {
|
||||
body { height: auto; min-height: 100vh; overflow-y: auto; }
|
||||
.upload-main { height: auto; min-height: 100vh; place-items: start center; overflow: visible; }
|
||||
.desktop-wrap {
|
||||
--window-height: 680px;
|
||||
grid-template-columns: minmax(0, 820px);
|
||||
grid-template-rows: var(--window-height) auto;
|
||||
width: min(820px, 100%);
|
||||
max-width: 820px;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.side-stack {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 350px 210px 132px;
|
||||
overflow: visible;
|
||||
}
|
||||
.side-panel,
|
||||
.helper-window {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.desktop-wrap { --window-height: 780px; }
|
||||
.side-stack { grid-template-rows: 372px 230px 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.upload-main {
|
||||
height: auto;
|
||||
min-height: 100dvh;
|
||||
place-items: stretch;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
.desktop-wrap {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
min-height: 100dvh;
|
||||
gap: 10px;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
overflow: visible;
|
||||
}
|
||||
.upload-window {
|
||||
min-height: 100dvh;
|
||||
height: auto;
|
||||
width: 100vw;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.side-stack {
|
||||
grid-template-rows: auto auto auto;
|
||||
padding: 0 6px 12px;
|
||||
}
|
||||
.side-panel:first-child { min-height: 360px; }
|
||||
.side-panel:nth-child(2) { min-height: 210px; }
|
||||
.helper-window { min-height: 128px; }
|
||||
.upload-header { grid-template-columns: 1fr; }
|
||||
.upload-panel { margin: 0 6px 8px; padding: 10px; }
|
||||
.upload-dropzone { min-height: 118px; padding: 14px 10px; }
|
||||
.upload-primary { font-size: 16px; }
|
||||
.upload-details { flex-wrap: wrap; gap: 4px; }
|
||||
.upload-file-count { margin-left: 0; width: 100%; }
|
||||
.upload-file-row { grid-template-columns: 22px minmax(0, 1fr) 58px 28px; padding: 4px 5px; font-size: 12px; }
|
||||
.upload-result { grid-template-columns: 1fr 72px; }
|
||||
.upload-result-label { grid-column: 1 / 3; }
|
||||
.upload-actions { justify-content: stretch; }
|
||||
.upload-actions .win98-button { flex: 1; min-width: 0; }
|
||||
.menu-bar { overflow-x: auto; }
|
||||
.menu-popup { position: fixed; left: 6px; right: 6px; top: 50px; min-width: 0; }
|
||||
.popup-window {
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: none;
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.popup-window .win98-titlebar { height: 32px; }
|
||||
.popup-close { width: 28px; height: 24px; font-size: 18px; font-weight: bold; }
|
||||
.popup-body { max-height: calc(100dvh - 40px); }
|
||||
.popup-window.is-visible { animation: popup-open-mobile-v10 160ms steps(5, end); }
|
||||
@keyframes popup-open-mobile-v10 { from { transform: translateY(10px); opacity: .35; } to { transform: translateY(0); opacity: 1; } }
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
:root { --base-font-size: 13px; }
|
||||
.win98-titlebar h1 { font-size: 13px; }
|
||||
.upload-file-size { display: none; }
|
||||
.upload-file-row { grid-template-columns: 22px minmax(0, 1fr) 28px; }
|
||||
.upload-file-remove { grid-column: 3; }
|
||||
.upload-progress { grid-column: 2 / 3; }
|
||||
}
|
||||
50
static/css/upload/sidebar.css
Normal file
50
static/css/upload/sidebar.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.upload-statusbar {
|
||||
grid-template-columns: 1fr 100px;
|
||||
}
|
||||
|
||||
.side-stack {
|
||||
width: var(--side-width);
|
||||
min-width: var(--side-width);
|
||||
max-width: var(--side-width);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: var(--side-width);
|
||||
grid-template-rows: 350px 210px 1fr;
|
||||
gap: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.side-panel,
|
||||
.helper-window {
|
||||
width: var(--side-width);
|
||||
min-width: var(--side-width);
|
||||
max-width: var(--side-width);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 3px 4px 0 rgba(0,0,0,.38);
|
||||
}
|
||||
|
||||
.side-body,
|
||||
.helper-body,
|
||||
.popup-body {
|
||||
margin: 0 6px 6px;
|
||||
padding: 9px;
|
||||
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: 15px;
|
||||
}
|
||||
|
||||
.side-body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
54
static/css/upload/terminal.css
Normal file
54
static/css/upload/terminal.css
Normal file
@@ -0,0 +1,54 @@
|
||||
.terminal-box {
|
||||
flex: 1 1 auto;
|
||||
min-height: 104px;
|
||||
max-height: 134px;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
color: #b4efbd;
|
||||
background-color: #030403;
|
||||
background-image: repeating-linear-gradient(transparent 0 4px, rgba(0,255,102,.018) 4px 6px);
|
||||
border: 0;
|
||||
box-shadow: inset 1px 1px 0 #000000, inset -1px -1px 0 rgba(255,255,255,.22);
|
||||
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.terminal-box::after {
|
||||
content: "█";
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
color: #7dff8a;
|
||||
animation: terminal-cursor 1s steps(2, end) infinite;
|
||||
}
|
||||
|
||||
.terminal-muted {
|
||||
color: #79ad83;
|
||||
}
|
||||
|
||||
.terminal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.terminal-copy-button {
|
||||
min-width: 148px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.helper-body {
|
||||
height: calc(100% - 34px);
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
1265
static/js/app.js
1265
static/js/app.js
File diff suppressed because it is too large
Load Diff
@@ -15,14 +15,7 @@ const zipOnly = boxPanel && boxPanel.dataset.zipOnly === "true";
|
||||
let contextFile = null;
|
||||
let lastStatusSignature = "";
|
||||
|
||||
function htmlEscape(value) {
|
||||
return String(value || "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
const htmlEscape = window.WarpBoxUI.htmlEscape;
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
window.WarpBoxUI.toast(message, type, { target: toast });
|
||||
@@ -302,23 +295,39 @@ function startStagedPolling(baseMS) {
|
||||
window.setTimeout(tick, stages[0].interval);
|
||||
}
|
||||
|
||||
function runBoxAction(action) {
|
||||
const actions = {
|
||||
"fake-close": () => showToast("Close clicked. The download window is emotionally attached.", "warning"),
|
||||
minimize: () => showToast("Minimize clicked. WarpBox refuses to disappear quietly."),
|
||||
"toggle-fit": () => {
|
||||
document.body.classList.toggle("fit-window");
|
||||
showToast("Maximize clicked. The window is doing its best.");
|
||||
},
|
||||
};
|
||||
|
||||
actions[action]?.();
|
||||
}
|
||||
|
||||
function runContextAction(action, item) {
|
||||
const actions = {
|
||||
preview: () => previewFile(item),
|
||||
download: () => downloadFile(item),
|
||||
properties: () => showProperties(item),
|
||||
};
|
||||
|
||||
actions[action]?.();
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
if (action) runBoxAction(action);
|
||||
|
||||
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);
|
||||
runContextAction(contextAction, item);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
139
static/js/upload/api.js
Normal file
139
static/js/upload/api.js
Normal file
@@ -0,0 +1,139 @@
|
||||
async function createBox() {
|
||||
const response = await fetch("/box", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
retention_key: el.expiry?.value || defaultRetention,
|
||||
password: el.password?.value || "",
|
||||
allow_zip: isOneTimeDownloadSelected() || !el.allowZip || el.allowZip.checked,
|
||||
files: files.map((item) => ({ name: item.displayName, size: item.file.size })),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await readJSON(response);
|
||||
if (!response.ok) throw new Error(result.error || "Could not create upload box");
|
||||
return result;
|
||||
}
|
||||
|
||||
async function readJSON(response) {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function markFileStatus(item, status) {
|
||||
if (!item.boxID || !item.boxFile) return;
|
||||
try {
|
||||
await fetch(`/box/${item.boxID}/files/${item.boxFile.id}/status`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
} catch (_) {
|
||||
// Best effort only. The upload endpoint also marks hard failures.
|
||||
}
|
||||
}
|
||||
|
||||
function setFileFailed(item, message) {
|
||||
item.failed = true;
|
||||
item.uploaded = false;
|
||||
item.error = message || "Failed to upload";
|
||||
item.loaded = item.file.size;
|
||||
item.row?.classList.remove("is-working", "is-uploaded");
|
||||
item.row?.classList.add("is-failed");
|
||||
if (item.row) item.row.title = item.error;
|
||||
setRowProgress(item, 100);
|
||||
updateOverallProgress();
|
||||
}
|
||||
|
||||
function markCompletedImpact(item) {
|
||||
const key = item.boxFile?.id || item.displayName;
|
||||
if (completedImpactKeys.has(key)) return;
|
||||
completedImpactKeys.add(key);
|
||||
flashProgressBar(item.row?.querySelector(".upload-progress-bar"));
|
||||
}
|
||||
|
||||
function uploadFile(item, onComplete) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const formData = new FormData();
|
||||
formData.append("file", item.file, item.displayName);
|
||||
|
||||
xhr.open("POST", item.boxFile.upload_path);
|
||||
|
||||
xhr.upload.addEventListener("loadstart", () => {
|
||||
item.loaded = 0;
|
||||
item.failed = false;
|
||||
item.uploaded = false;
|
||||
item.row?.classList.remove("is-failed", "is-uploaded");
|
||||
item.row?.classList.add("is-working");
|
||||
setRowProgress(item, 2);
|
||||
updateOverallProgress();
|
||||
});
|
||||
|
||||
xhr.upload.addEventListener("progress", (event) => {
|
||||
if (!event.lengthComputable) return;
|
||||
item.loaded = Math.min(event.loaded, item.file.size);
|
||||
const percent = (event.loaded / event.total) * 100;
|
||||
setRowProgress(item, percent >= 100 ? 99 : percent);
|
||||
updateOverallProgress();
|
||||
});
|
||||
|
||||
xhr.addEventListener("load", async () => {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
let message = "Upload failed";
|
||||
try {
|
||||
message = JSON.parse(xhr.responseText).error || message;
|
||||
} catch (_) {}
|
||||
setFileFailed(item, message);
|
||||
await markFileStatus(item, "failed");
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
|
||||
item.uploaded = true;
|
||||
item.failed = false;
|
||||
item.loaded = item.file.size;
|
||||
item.row?.classList.remove("is-working", "is-failed");
|
||||
item.row?.classList.add("is-uploaded");
|
||||
if (item.row) item.row.title = "Uploaded";
|
||||
setRowProgress(item, 100);
|
||||
markCompletedImpact(item);
|
||||
|
||||
try {
|
||||
const result = JSON.parse(xhr.responseText);
|
||||
if (result.file) {
|
||||
item.boxFile = result.file;
|
||||
const icon = item.row?.querySelector(".upload-file-icon");
|
||||
if (icon && result.file.thumbnail_path) {
|
||||
item.row.classList.add("has-thumbnail");
|
||||
icon.src = result.file.thumbnail_path;
|
||||
} else if (icon && result.file.icon_path && !item.previewURL) {
|
||||
icon.src = result.file.icon_path;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
updateOverallProgress();
|
||||
onComplete();
|
||||
resolve();
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", async () => {
|
||||
setFileFailed(item, "Network error while uploading");
|
||||
await markFileStatus(item, "failed");
|
||||
reject(new Error("Network error while uploading"));
|
||||
});
|
||||
|
||||
xhr.addEventListener("abort", async () => {
|
||||
setFileFailed(item, "Upload cancelled");
|
||||
await markFileStatus(item, "failed");
|
||||
reject(new Error("Upload cancelled"));
|
||||
});
|
||||
|
||||
markFileStatus(item, "uploading");
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
232
static/js/upload/dom.js
Normal file
232
static/js/upload/dom.js
Normal file
@@ -0,0 +1,232 @@
|
||||
function setStatus(message) {
|
||||
if (el.statusText) el.statusText.textContent = message;
|
||||
}
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
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, .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.";
|
||||
}
|
||||
if (control.disabled || control.readOnly || control.getAttribute("aria-disabled") === "true") {
|
||||
return control.dataset.disabledReason || control.title || "This control is disabled right now.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function announceDisabledReason(event) {
|
||||
const reason = disabledReasonFor(event.target);
|
||||
if (!reason) return false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeMenus();
|
||||
showToast(reason, "warning");
|
||||
setStatus(reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
function stopStatusAnimation() {
|
||||
if (statusTimer) {
|
||||
clearInterval(statusTimer);
|
||||
statusTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function animateUploadStatus(getPrefix) {
|
||||
let dotCount = 0;
|
||||
stopStatusAnimation();
|
||||
statusTimer = setInterval(() => {
|
||||
dotCount = (dotCount % 3) + 1;
|
||||
setStatus(`${getPrefix()} Uploading${".".repeat(dotCount)}`);
|
||||
}, 350);
|
||||
}
|
||||
|
||||
function setShareUrl(url) {
|
||||
shareUrl = url ? new URL(url, window.location.origin).toString() : "";
|
||||
if (!el.shareLink || !el.copyButton) return;
|
||||
el.shareLink.textContent = shareUrl || "Not created yet";
|
||||
el.shareLink.href = shareUrl || "#";
|
||||
el.shareLink.title = shareUrl;
|
||||
el.shareLink.classList.toggle("is-empty", !shareUrl);
|
||||
el.shareLink.setAttribute("aria-disabled", shareUrl ? "false" : "true");
|
||||
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();
|
||||
}
|
||||
|
||||
function setOverallProgress(percent) {
|
||||
const clamped = Math.max(0, Math.min(100, percent));
|
||||
const display = `${Math.round(clamped)}%`;
|
||||
if (el.overallBar) el.overallBar.style.width = display;
|
||||
if (el.overallPercent) el.overallPercent.textContent = display;
|
||||
}
|
||||
|
||||
function flashProgressBar(bar) {
|
||||
if (!bar) return;
|
||||
bar.classList.remove("just-completed");
|
||||
void bar.offsetWidth;
|
||||
bar.classList.add("just-completed");
|
||||
setTimeout(() => bar.classList.remove("just-completed"), 620);
|
||||
}
|
||||
|
||||
function setRowProgress(item, percent) {
|
||||
const bar = item.row?.querySelector(".upload-progress-bar");
|
||||
if (bar) bar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
||||
}
|
||||
|
||||
function updateCurrentStep() {
|
||||
const hasFiles = files.length > 0;
|
||||
const allDone = hasFiles && files.every((item) => item.uploaded);
|
||||
el.dropzone?.classList.toggle("is-current-step", uploadsEnabled && !hasFiles && !uploadLocked);
|
||||
el.startButton?.classList.toggle("is-current-step", uploadsEnabled && hasFiles && !allDone && !uploadLocked && !hasQuotaError());
|
||||
document.querySelector(".upload-result")?.classList.toggle("is-current-step", allDone && Boolean(shareUrl));
|
||||
}
|
||||
|
||||
function quotaWarningMessage(incoming = []) {
|
||||
const combined = [...files, ...incoming];
|
||||
const tooBig = maxFileBytes ? combined.filter((item) => item.file.size > maxFileBytes) : [];
|
||||
const total = combined.reduce((sum, item) => sum + item.file.size, 0);
|
||||
if (tooBig.length) {
|
||||
const list = tooBig.slice(0, 4).map((item) => `${item.displayName} (${formatBytes(item.file.size)})`).join(", ");
|
||||
const more = tooBig.length > 4 ? ` and ${tooBig.length - 4} more` : "";
|
||||
return `These files are over the single-file limit of ${formatBytes(maxFileBytes)}: ${list}${more}. Remove them before uploading.`;
|
||||
}
|
||||
if (maxBoxBytes && total > maxBoxBytes) {
|
||||
return `This box is ${formatBytes(total - maxBoxBytes)} over the ${formatBytes(maxBoxBytes)} limit. Remove some files before uploading.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function updateLimitHint() {
|
||||
if (!el.limitHint) return;
|
||||
const parts = [];
|
||||
if (maxBoxBytes) parts.push(`Max box: ${formatBytes(maxBoxBytes)}`);
|
||||
if (maxFileBytes) parts.push(`max file: ${formatBytes(maxFileBytes)}`);
|
||||
parts.push("links expire automatically");
|
||||
el.limitHint.textContent = parts.join(" · ");
|
||||
}
|
||||
|
||||
function updateQuota() {
|
||||
const used = totalBytes();
|
||||
const limitText = maxBoxBytes ? ` / ${formatBytes(maxBoxBytes)}` : "";
|
||||
const overQuota = isOverBoxQuota();
|
||||
const overFile = oversizedFiles().length > 0;
|
||||
const percent = maxBoxBytes ? Math.min(100, Math.round((used / maxBoxBytes) * 100)) : 0;
|
||||
document.querySelector(".upload-quota")?.classList.toggle("is-quota-warning", overQuota || overFile);
|
||||
if (el.boxSpaceText) el.boxSpaceText.textContent = `${formatBytes(used)}${limitText}${overQuota ? " - over quota" : ""}`;
|
||||
if (el.boxSpaceBar) {
|
||||
el.boxSpaceBar.style.width = `${percent}%`;
|
||||
el.boxSpaceBar.classList.toggle("is-over-quota", overQuota || overFile);
|
||||
}
|
||||
}
|
||||
|
||||
function updateQueueSummary() {
|
||||
const count = files.length;
|
||||
if (el.queueLabel) el.queueLabel.textContent = count ? `${count} file${count === 1 ? "" : "s"} selected` : "No files selected";
|
||||
if (el.queueSize) el.queueSize.textContent = `${formatBytes(totalBytes())} total`;
|
||||
}
|
||||
|
||||
function updateOverallProgress() {
|
||||
const uploadedCount = files.filter((item) => item.uploaded).length;
|
||||
const percent = overallProgress();
|
||||
setOverallProgress(percent >= 100 && uploadedCount < files.length ? 99 : percent);
|
||||
if (percent >= 100 && files.length && !overallImpactDone) {
|
||||
overallImpactDone = true;
|
||||
flashProgressBar(el.overallBar);
|
||||
}
|
||||
}
|
||||
|
||||
function createFileRow(item, index) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "upload-file-row";
|
||||
row.dataset.index = String(index);
|
||||
row.classList.toggle("has-thumbnail", Boolean(item.previewURL));
|
||||
row.classList.toggle("is-too-large", maxFileBytes > 0 && item.file.size > maxFileBytes);
|
||||
row.classList.toggle("is-working", item.loaded > 0 && !item.uploaded && !item.failed);
|
||||
row.classList.toggle("is-uploaded", item.uploaded);
|
||||
row.classList.toggle("is-failed", item.failed);
|
||||
row.title = item.error || "";
|
||||
|
||||
const icon = document.createElement("img");
|
||||
icon.className = "upload-file-icon";
|
||||
icon.src = item.previewURL || iconForFile(item.file);
|
||||
icon.alt = "";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "upload-file-name";
|
||||
name.textContent = item.displayName;
|
||||
name.title = item.displayName;
|
||||
|
||||
const size = document.createElement("span");
|
||||
size.className = "upload-file-size";
|
||||
size.textContent = formatBytes(item.file.size);
|
||||
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "win98-button upload-file-remove";
|
||||
remove.type = "button";
|
||||
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 = 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");
|
||||
progress.className = "upload-progress";
|
||||
progress.setAttribute("aria-label", `Upload progress ${Math.round(item.file.size ? (item.loaded / item.file.size) * 100 : 0)} percent`);
|
||||
|
||||
const progressBar = document.createElement("span");
|
||||
progressBar.className = "upload-progress-bar";
|
||||
progressBar.style.width = `${item.uploaded ? 100 : item.failed ? 100 : Math.max(0, Math.min(100, item.file.size ? (item.loaded / item.file.size) * 100 : 0))}%`;
|
||||
progress.append(progressBar);
|
||||
|
||||
row.append(icon, name, size, remove, progress);
|
||||
item.row = row;
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderFiles() {
|
||||
if (!el.fileList) return;
|
||||
el.fileList.replaceChildren();
|
||||
|
||||
if (!files.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "upload-empty-state";
|
||||
empty.textContent = uploadsEnabled
|
||||
? "No files in the box yet. Drop files here, use File > Add files, or click the dropzone."
|
||||
: "Guest uploads are disabled.";
|
||||
el.fileList.append(empty);
|
||||
} else {
|
||||
const fragment = document.createDocumentFragment();
|
||||
files.forEach((item, index) => fragment.append(createFileRow(item, index)));
|
||||
el.fileList.append(fragment);
|
||||
}
|
||||
|
||||
updateQueueSummary();
|
||||
updateQuota();
|
||||
updateOverallProgress();
|
||||
updateTerminal();
|
||||
updateDisabledReasons();
|
||||
updateCurrentStep();
|
||||
}
|
||||
237
static/js/upload/events.js
Normal file
237
static/js/upload/events.js
Normal file
@@ -0,0 +1,237 @@
|
||||
function runUploadAction(action) {
|
||||
const actions = {
|
||||
browse: () => el.fileInput?.click(),
|
||||
"start-upload": () => startUpload(),
|
||||
"copy-link": () => copyText("Share URL", shareUrl, shareUrl),
|
||||
clear: () => confirmClearQueue(),
|
||||
"toggle-delete-once": () => {
|
||||
if (!el.expiry?.querySelector(`option[value="${oneTimeRetentionKey}"]`)) return;
|
||||
el.expiry.value = isOneTimeDownloadSelected() ? defaultRetention : oneTimeRetentionKey;
|
||||
syncZipForRetention();
|
||||
saveSettings();
|
||||
syncMenuChecks();
|
||||
updateTerminal();
|
||||
},
|
||||
"random-password": () => randomPassword(),
|
||||
"random-box-name": () => randomBoxName(),
|
||||
"clear-password": () => {
|
||||
if (!el.password || uploadLocked) return;
|
||||
el.password.value = "";
|
||||
saveSettings();
|
||||
updateTerminal();
|
||||
},
|
||||
"toggle-page": () => {
|
||||
if (!el.downloadPage || uploadLocked) return;
|
||||
el.downloadPage.checked = !el.downloadPage.checked;
|
||||
saveSettings();
|
||||
syncMenuChecks();
|
||||
},
|
||||
help: () => openDoc("faq"),
|
||||
"side-help": () => {
|
||||
openDoc("faq");
|
||||
showToast("Terminal help opened. Copy the command and feed it files.");
|
||||
},
|
||||
"coming-soon": () => showToast("Coming Soon, not implemented just yet."),
|
||||
"fake-close": () => showToast("Close button denied. The upload window is staying open.", "warning"),
|
||||
minimize: () => showToast("Minimize requested. WarpBox stays visible so your queue is safe."),
|
||||
"toggle-fit": () => {
|
||||
document.body.classList.toggle("fit-window");
|
||||
showToast("Maximize requested. The pixel rectangle feels important now.");
|
||||
},
|
||||
"side-close": () => showToast("Box Options refuses to leave. Settings stay visible."),
|
||||
"side-folder-close": () => showToast("The folder window saw that click and chose denial."),
|
||||
};
|
||||
|
||||
actions[action]?.();
|
||||
}
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (announceDisabledReason(event)) return;
|
||||
|
||||
const menuButton = event.target.closest(".menu-button");
|
||||
if (menuButton) {
|
||||
const item = menuButton.closest(".menu-item");
|
||||
const isOpen = item.classList.contains("is-open");
|
||||
closeMenus();
|
||||
item.classList.toggle("is-open", !isOpen);
|
||||
menuButton.setAttribute("aria-expanded", String(!isOpen));
|
||||
return;
|
||||
}
|
||||
|
||||
const action = event.target.closest("[data-action]")?.dataset.action;
|
||||
if (action) {
|
||||
closeMenus();
|
||||
runUploadAction(action);
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = event.target.closest("[data-doc]")?.dataset.doc;
|
||||
if (doc) {
|
||||
openDoc(doc);
|
||||
return;
|
||||
}
|
||||
|
||||
const remove = event.target.closest("[data-remove]");
|
||||
if (remove) {
|
||||
removeFile(Number(remove.dataset.remove));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.id === "duplicate-append") appendPendingDuplicates();
|
||||
if (event.target.id === "duplicate-skip") {
|
||||
pendingDuplicateFiles = [];
|
||||
closeDoc();
|
||||
showToast("Duplicate files skipped.");
|
||||
}
|
||||
if (event.target.id === "confirm-clear-yes") {
|
||||
closeDoc();
|
||||
clearQueue();
|
||||
}
|
||||
if (event.target.id === "confirm-clear-no" || event.target.id === "fallback-close") closeDoc();
|
||||
|
||||
if (!event.target.closest(".menu-item")) {
|
||||
closeMenus();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mousedown", (event) => {
|
||||
announceDisabledReason(event);
|
||||
}, true);
|
||||
|
||||
document.querySelectorAll(".menu-item").forEach((item) => {
|
||||
item.addEventListener("mouseenter", () => {
|
||||
if (!document.querySelector(".menu-item.is-open")) return;
|
||||
closeMenus();
|
||||
item.classList.add("is-open");
|
||||
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
});
|
||||
|
||||
el.fileInput?.addEventListener("change", () => addFiles(el.fileInput.files));
|
||||
|
||||
[el.dropSurface, el.dropzone].filter(Boolean).forEach((target) => {
|
||||
target.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
el.dropzone?.classList.add("is-dragging");
|
||||
});
|
||||
target.addEventListener("dragleave", () => el.dropzone?.classList.remove("is-dragging"));
|
||||
target.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
el.dropzone?.classList.remove("is-dragging");
|
||||
addFiles(event.dataTransfer.files);
|
||||
});
|
||||
});
|
||||
|
||||
el.dropzone?.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
el.fileInput?.click();
|
||||
}
|
||||
});
|
||||
|
||||
el.form?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
startUpload();
|
||||
});
|
||||
|
||||
el.copyButton?.addEventListener("click", () => copyText("Share URL", shareUrl, shareUrl));
|
||||
el.copyCurlButton?.addEventListener("click", () => copyText("cURL command", getCurlCommand({ full: true })));
|
||||
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();
|
||||
if (control === el.customSlug) {
|
||||
const clean = sanitizeSlugInput(el.customSlug.value);
|
||||
if (el.customSlug.value !== clean) el.customSlug.value = clean;
|
||||
el.customSlug.dataset.auto = "false";
|
||||
}
|
||||
if (control === el.apiKeyInput) validateApiKeyField();
|
||||
saveSettings();
|
||||
updateTerminal();
|
||||
});
|
||||
control.addEventListener("change", () => {
|
||||
if (control === el.expiry) syncZipForRetention();
|
||||
if (control === el.apiKeyMode) syncApiKeyField();
|
||||
saveSettings();
|
||||
syncMenuChecks();
|
||||
updateTerminal();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeDoc();
|
||||
closeMenus();
|
||||
}
|
||||
if (event.key === "F1") {
|
||||
event.preventDefault();
|
||||
openDoc("faq");
|
||||
}
|
||||
if (event.ctrlKey && !event.shiftKey && !event.altKey) {
|
||||
const key = event.key.toLowerCase();
|
||||
if (key === "o") {
|
||||
event.preventDefault();
|
||||
el.fileInput?.click();
|
||||
}
|
||||
if (key === "u") {
|
||||
event.preventDefault();
|
||||
startUpload();
|
||||
}
|
||||
if (key === "k") {
|
||||
event.preventDefault();
|
||||
copyText("cURL command", getCurlCommand({ full: true }));
|
||||
}
|
||||
if (key === "l") {
|
||||
event.preventDefault();
|
||||
copyText("Share URL", shareUrl, shareUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
files.forEach((item) => {
|
||||
if (item.previewURL) URL.revokeObjectURL(item.previewURL);
|
||||
});
|
||||
});
|
||||
108
static/js/upload/files.js
Normal file
108
static/js/upload/files.js
Normal file
@@ -0,0 +1,108 @@
|
||||
function duplicateFileReport(incoming = []) {
|
||||
const used = new Set(files.map((item) => normalizedFileName(item.displayName)));
|
||||
const duplicates = [];
|
||||
const unique = [];
|
||||
incoming.forEach((item) => {
|
||||
const key = normalizedFileName(item.displayName);
|
||||
if (used.has(key)) {
|
||||
duplicates.push(item);
|
||||
return;
|
||||
}
|
||||
used.add(key);
|
||||
unique.push(item);
|
||||
});
|
||||
return { unique, duplicates };
|
||||
}
|
||||
|
||||
function addFiles(fileList) {
|
||||
if (!uploadsEnabled) {
|
||||
showToast("Guest uploads are disabled.", "warning");
|
||||
return;
|
||||
}
|
||||
if (uploadLocked) {
|
||||
showToast("This box is sealed. Clear it to create a fresh upload.", "warning");
|
||||
return;
|
||||
}
|
||||
const incoming = Array.from(fileList || []).map((file) => makeQueuedFile(file));
|
||||
if (!incoming.length) return;
|
||||
|
||||
const { unique, duplicates } = duplicateFileReport(incoming);
|
||||
if (unique.length) {
|
||||
files.push(...unique);
|
||||
setShareUrl("");
|
||||
renderFiles();
|
||||
const warning = quotaWarningMessage();
|
||||
if (warning) showWarningDialog("Quota warning", warning);
|
||||
}
|
||||
if (duplicates.length) showDuplicateDialog(duplicates);
|
||||
|
||||
if (unique.length) setStatus(`${unique.length} file${unique.length === 1 ? "" : "s"} added to queue`);
|
||||
if (duplicates.length && !unique.length) setStatus(`${duplicates.length} duplicate file${duplicates.length === 1 ? "" : "s"} need your choice`);
|
||||
}
|
||||
|
||||
function showDuplicateDialog(duplicates) {
|
||||
pendingDuplicateFiles = duplicates;
|
||||
const list = duplicates.map((item) => `<li><strong>${htmlEscape(item.displayName)}</strong> <span>${formatBytes(item.file.size)}</span></li>`).join("");
|
||||
showTemplatePopup("Duplicate file names", "duplicate", { list })
|
||||
.then(() => document.querySelector("#duplicate-append")?.focus());
|
||||
showToast("Duplicate names found. Choose skip or append numbers.", "warning");
|
||||
}
|
||||
|
||||
function appendPendingDuplicates() {
|
||||
if (!pendingDuplicateFiles.length) return;
|
||||
const used = new Set(files.map((item) => normalizedFileName(item.displayName)));
|
||||
pendingDuplicateFiles.forEach((item) => {
|
||||
item.displayName = nextIncrementedFileName(item.displayName, used);
|
||||
files.push(item);
|
||||
});
|
||||
const count = pendingDuplicateFiles.length;
|
||||
pendingDuplicateFiles = [];
|
||||
closeDoc();
|
||||
setShareUrl("");
|
||||
renderFiles();
|
||||
showToast("Duplicate files added with numbered names.", "info");
|
||||
setStatus(`${count} duplicate file${count === 1 ? "" : "s"} added with numbered names`);
|
||||
}
|
||||
|
||||
function removeFile(index) {
|
||||
if (uploadLocked) {
|
||||
showToast("Box already created. Clear it before editing the queue.", "warning");
|
||||
return;
|
||||
}
|
||||
const [removed] = files.splice(index, 1);
|
||||
if (removed?.previewURL) URL.revokeObjectURL(removed.previewURL);
|
||||
setShareUrl("");
|
||||
renderFiles();
|
||||
setStatus("File removed from queue");
|
||||
}
|
||||
|
||||
function clearQueue() {
|
||||
files.forEach((item) => {
|
||||
if (item.previewURL) URL.revokeObjectURL(item.previewURL);
|
||||
});
|
||||
files = [];
|
||||
pendingDuplicateFiles = [];
|
||||
uploadLocked = false;
|
||||
completedImpactKeys = new Set();
|
||||
overallImpactDone = false;
|
||||
stopStatusAnimation();
|
||||
setBoxOptionsLocked(false);
|
||||
setShareUrl("");
|
||||
if (el.fileInput) {
|
||||
el.fileInput.value = "";
|
||||
el.fileInput.disabled = !uploadsEnabled;
|
||||
}
|
||||
el.dropzone?.classList.remove("is-locked");
|
||||
renderFiles();
|
||||
setStatus(uploadsEnabled ? "Queue cleared" : "Guest uploads are disabled");
|
||||
showToast("Queue cleared.");
|
||||
}
|
||||
|
||||
function confirmClearQueue() {
|
||||
if (!files.length && !shareUrl) {
|
||||
showToast("Nothing to clear.");
|
||||
return;
|
||||
}
|
||||
showTemplatePopup("Clear WarpBox?", "clear")
|
||||
.then(() => document.querySelector("#confirm-clear-no")?.focus());
|
||||
}
|
||||
192
static/js/upload/options.js
Normal file
192
static/js/upload/options.js
Normal file
@@ -0,0 +1,192 @@
|
||||
function isOneTimeDownloadSelected() {
|
||||
return el.expiry?.value === oneTimeRetentionKey;
|
||||
}
|
||||
|
||||
function syncZipForRetention() {
|
||||
if (!el.allowZip) return;
|
||||
if (isOneTimeDownloadSelected()) {
|
||||
el.allowZip.checked = true;
|
||||
el.allowZip.disabled = true;
|
||||
} else if (!uploadLocked) {
|
||||
el.allowZip.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setBoxOptionsLocked(locked) {
|
||||
const controls = [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);
|
||||
el.optionsForm?.classList.toggle("is-locked", locked);
|
||||
controls.forEach((control) => {
|
||||
control.dataset.disabledReason = locked ? "Box Options are locked because this box was already created. Press Clear to start another upload." : "";
|
||||
if (control.tagName === "INPUT" && !["checkbox", "radio", "file"].includes(control.type)) {
|
||||
control.readOnly = locked;
|
||||
} else {
|
||||
control.disabled = locked;
|
||||
}
|
||||
});
|
||||
if (el.password) el.password.type = locked ? "password" : "text";
|
||||
if (!locked) {
|
||||
syncZipForRetention();
|
||||
syncApiKeyField();
|
||||
}
|
||||
updateDisabledReasons();
|
||||
}
|
||||
|
||||
function updateDisabledReasons() {
|
||||
if (el.startButton) {
|
||||
let reason = "";
|
||||
if (!uploadsEnabled) reason = "Guest uploads are disabled.";
|
||||
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 = false;
|
||||
el.startButton.setAttribute("aria-disabled", reason ? "true" : "false");
|
||||
el.startButton.dataset.disabledReason = reason;
|
||||
el.startButton.title = reason;
|
||||
}
|
||||
if (el.fileInput) {
|
||||
el.fileInput.dataset.disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : "");
|
||||
}
|
||||
if (el.dropzone) {
|
||||
el.dropzone.dataset.disabledReason = uploadLocked ? "The current box is sealed after upload. Press Clear to start a new box." : (!uploadsEnabled ? "Guest uploads are disabled." : "");
|
||||
}
|
||||
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 = {
|
||||
maxViews: el.maxViews?.value || "",
|
||||
allowPreview: Boolean(el.allowPreview?.checked),
|
||||
keepFilenames: Boolean(el.keepFilenames?.checked),
|
||||
privateBox: Boolean(el.privateBox?.checked),
|
||||
apiKeyMode: Boolean(el.apiKeyMode?.checked),
|
||||
apiKey,
|
||||
};
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
let settings = {};
|
||||
try {
|
||||
settings = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}");
|
||||
} catch (_) {}
|
||||
if (el.maxViews) el.maxViews.value = settings.maxViews || "";
|
||||
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 = validApiKey(settings.apiKey || "") ? settings.apiKey : "";
|
||||
syncZipForRetention();
|
||||
syncApiKeyField();
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncMenuChecks() {
|
||||
updateDisabledReasons();
|
||||
}
|
||||
|
||||
function syncApiKeyField() {
|
||||
const enabled = Boolean(el.apiKeyMode?.checked) && !uploadLocked;
|
||||
el.apiKeyRow?.classList.toggle("is-visible", Boolean(el.apiKeyMode?.checked));
|
||||
if (el.apiKeyInput) {
|
||||
el.apiKeyInput.disabled = !enabled;
|
||||
el.apiKeyInput.dataset.disabledReason = enabled ? "" : "Enable Use API key for larger quota before typing an API key.";
|
||||
}
|
||||
validateApiKeyField();
|
||||
}
|
||||
|
||||
function validateApiKeyField() {
|
||||
if (!el.apiKeyInput || !el.apiKeyState) return;
|
||||
clearTimeout(apiKeyTimer);
|
||||
const wrapper = el.apiKeyInput.closest(".api-key-field");
|
||||
wrapper?.classList.remove("is-checking");
|
||||
|
||||
if (!el.apiKeyMode?.checked) {
|
||||
el.apiKeyState.textContent = "";
|
||||
return;
|
||||
}
|
||||
const value = el.apiKeyInput.value.trim();
|
||||
if (!value) {
|
||||
el.apiKeyState.textContent = "waiting";
|
||||
saveSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
el.apiKeyInput.disabled = true;
|
||||
wrapper?.classList.add("is-checking");
|
||||
el.apiKeyState.textContent = "checking";
|
||||
apiKeyTimer = setTimeout(() => {
|
||||
wrapper?.classList.remove("is-checking");
|
||||
el.apiKeyInput.disabled = uploadLocked;
|
||||
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()
|
||||
.replace(/[^a-z0-9-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 32);
|
||||
}
|
||||
|
||||
function sanitizeSlugInput(value) {
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.slice(0, 32);
|
||||
}
|
||||
|
||||
function syncSlugFromName(force = false) {
|
||||
if (!el.customSlug || !el.boxName) return;
|
||||
if (force || !el.customSlug.value || el.customSlug.dataset.auto === "true") {
|
||||
el.customSlug.value = slugify(el.boxName.value);
|
||||
el.customSlug.dataset.auto = "true";
|
||||
}
|
||||
saveSettings();
|
||||
updateTerminal();
|
||||
}
|
||||
|
||||
function randomPassword() {
|
||||
if (!el.password || uploadLocked) return;
|
||||
el.password.value = `${Math.random().toString(36).slice(2, 8)}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
saveSettings();
|
||||
updateTerminal();
|
||||
setStatus("Generated a password");
|
||||
}
|
||||
|
||||
function randomBoxName() {
|
||||
if (!el.boxName || uploadLocked) return;
|
||||
const adjectives = ["Neon", "Turbo", "Quiet", "Cosmic", "Lucky", "Midnight", "Pixel", "Rapid"];
|
||||
const nouns = ["Floppy Disk", "Archive Box", "Packet Portal", "Upload Folder", "Cache Drive", "Release Bundle"];
|
||||
el.boxName.value = `${adjectives[Math.floor(Math.random() * adjectives.length)]} ${nouns[Math.floor(Math.random() * nouns.length)]}`;
|
||||
syncSlugFromName(true);
|
||||
setStatus("Generated a local box name");
|
||||
}
|
||||
88
static/js/upload/popups.js
Normal file
88
static/js/upload/popups.js
Normal file
@@ -0,0 +1,88 @@
|
||||
async function copyText(kind, value, openUrl = "") {
|
||||
if (!value) {
|
||||
showToast(`No ${kind.toLowerCase()} yet.`, "warning");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showToast(`${kind} copied to clipboard.`);
|
||||
setStatus(`Copied ${kind.toLowerCase()}`);
|
||||
} catch (_) {
|
||||
showCopyFallback(kind, value, openUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function showCopyFallback(kind, value, openUrl) {
|
||||
const openLink = openUrl ? `<a class="win98-button" href="${htmlEscape(openUrl)}" target="_blank" rel="noreferrer">Open</a>` : "";
|
||||
showTemplatePopup(`${kind} copy failed`, "copy-failed", {
|
||||
value: htmlEscape(value),
|
||||
openLink,
|
||||
});
|
||||
}
|
||||
|
||||
function quotaWarningHtml(message) {
|
||||
const tooLarge = oversizedFiles();
|
||||
const parts = [];
|
||||
if (tooLarge.length) {
|
||||
parts.push("<p class=\"quota-dialog-summary\"><strong>Single-file limit exceeded.</strong> Remove these files before uploading.</p>");
|
||||
parts.push(`<ol class="quota-dialog-list">${tooLarge.map((item) => `<li><strong>${htmlEscape(item.displayName)}</strong> <span>${formatBytes(item.file.size)} / max ${formatBytes(maxFileBytes)}</span></li>`).join("")}</ol>`);
|
||||
}
|
||||
if (isOverBoxQuota()) {
|
||||
parts.push(`<p class="quota-dialog-summary"><strong>Box quota exceeded.</strong> Current total is ${formatBytes(totalBytes())}. The limit is ${formatBytes(maxBoxBytes)}. Remove ${formatBytes(totalBytes() - maxBoxBytes)} or more.</p>`);
|
||||
}
|
||||
if (!parts.length) parts.push(`<p>${htmlEscape(message)}</p>`);
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function showWarningDialog(title, message) {
|
||||
showTemplatePopup(title, "warning", {
|
||||
title: htmlEscape(title),
|
||||
content: quotaWarningHtml(message),
|
||||
});
|
||||
}
|
||||
|
||||
function openPopup(title, html, about = false) {
|
||||
window.WarpBoxUI.openPopup(title, html, {
|
||||
about,
|
||||
popup: el.docPopup,
|
||||
title: el.docPopupTitle,
|
||||
body: el.docPopupBody,
|
||||
backdrop: el.modalBackdrop,
|
||||
});
|
||||
}
|
||||
|
||||
function closeDoc() {
|
||||
window.WarpBoxUI.closePopup({ popup: el.docPopup, backdrop: el.modalBackdrop });
|
||||
}
|
||||
|
||||
async function showTemplatePopup(title, templateName, data = {}, about = false) {
|
||||
try {
|
||||
const html = await window.WBPopups.renderTemplate(templateName, data);
|
||||
openPopup(title, html, about);
|
||||
} catch (error) {
|
||||
showToast(error.message || `Could not load ${title}.`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function popupTemplateData(name) {
|
||||
const data = { origin: window.location.origin };
|
||||
if (name !== "dailyQuota") return data;
|
||||
return {
|
||||
...data,
|
||||
boxLimit: maxBoxBytes ? formatBytes(maxBoxBytes) : "No configured limit",
|
||||
boxPercent: maxBoxBytes ? Math.min(100, Math.round((totalBytes() / maxBoxBytes) * 100)) : 0,
|
||||
fileLimit: maxFileBytes ? formatBytes(maxFileBytes) : "No configured limit",
|
||||
filePercent: oversizedFiles().length ? 100 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function openDoc(name) {
|
||||
try {
|
||||
const doc = await window.WBPopups.renderDoc(name, popupTemplateData(name));
|
||||
if (!doc) return;
|
||||
openPopup(doc.title, doc.html, doc.about);
|
||||
setStatus(`${doc.title} opened`);
|
||||
} catch (error) {
|
||||
showToast(error.message || "Could not load help window.", "error");
|
||||
}
|
||||
}
|
||||
160
static/js/upload/state.js
Normal file
160
static/js/upload/state.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const SETTINGS_KEY = "warpbox.upload.settings.v1";
|
||||
|
||||
const el = {
|
||||
form: document.querySelector("#upload-form"),
|
||||
fileInput: document.querySelector("#file-upload"),
|
||||
dropSurface: document.querySelector("#drop-surface"),
|
||||
dropzone: document.querySelector("#dropzone"),
|
||||
fileList: document.querySelector("#file-list"),
|
||||
queueLabel: document.querySelector("#queue-label"),
|
||||
queueSize: document.querySelector("#queue-size"),
|
||||
limitHint: document.querySelector("#limit-hint"),
|
||||
boxSpaceText: document.querySelector("#box-space-text"),
|
||||
boxSpaceBar: document.querySelector("#box-space-bar"),
|
||||
overallBar: document.querySelector("#overall-bar"),
|
||||
overallPercent: document.querySelector("#overall-percent"),
|
||||
shareLink: document.querySelector("#share-link"),
|
||||
copyButton: document.querySelector("#copy-button"),
|
||||
startButton: document.querySelector("#start-button"),
|
||||
statusText: document.querySelector("#status-text"),
|
||||
toast: document.querySelector("#toast"),
|
||||
terminal: document.querySelector("#terminal-box"),
|
||||
copyCurlButton: document.querySelector("#copy-curl-button"),
|
||||
docPopup: document.querySelector("#doc-popup"),
|
||||
modalBackdrop: document.querySelector("#modal-backdrop"),
|
||||
docPopupTitle: document.querySelector("#doc-popup-title"),
|
||||
docPopupBody: document.querySelector("#doc-popup-body"),
|
||||
docPopupClose: document.querySelector("#doc-popup-close"),
|
||||
expiry: document.querySelector("#expiry-select"),
|
||||
password: document.querySelector("#password-input"),
|
||||
optionsForm: document.querySelector("#box-options-form"),
|
||||
maxViews: document.querySelector("#max-views"),
|
||||
boxName: document.querySelector("#box-name"),
|
||||
customSlug: document.querySelector("#custom-slug"),
|
||||
downloadPage: document.querySelector("#download-page"),
|
||||
allowZip: document.querySelector("#allow-zip"),
|
||||
allowPreview: document.querySelector("#allow-preview"),
|
||||
keepFilenames: document.querySelector("#keep-filenames"),
|
||||
privateBox: document.querySelector("#private-box"),
|
||||
apiKeyMode: document.querySelector("#api-key-mode"),
|
||||
apiKeyInput: document.querySelector("#api-key-input"),
|
||||
apiKeyRow: document.querySelector("#api-key-row"),
|
||||
apiKeyState: document.querySelector("#api-key-state"),
|
||||
};
|
||||
|
||||
const uploadsEnabled = el.form?.dataset.uploadsEnabled === "true";
|
||||
const defaultRetention = el.form?.dataset.defaultRetention || "10s";
|
||||
const maxFileBytes = numberFromDataset(el.form?.dataset.maxFileBytes);
|
||||
const maxBoxBytes = numberFromDataset(el.form?.dataset.maxBoxBytes);
|
||||
const oneTimeRetentionKey = "one-time";
|
||||
|
||||
let files = [];
|
||||
let shareUrl = "";
|
||||
let uploadLocked = false;
|
||||
let statusTimer = null;
|
||||
let pendingDuplicateFiles = [];
|
||||
let apiKeyTimer = null;
|
||||
let completedImpactKeys = new Set();
|
||||
let overallImpactDone = false;
|
||||
|
||||
function numberFromDataset(value) {
|
||||
const number = Number.parseInt(value || "0", 10);
|
||||
return Number.isFinite(number) && number > 0 ? number : 0;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return "0 B";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let value = bytes;
|
||||
let unit = 0;
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
value /= 1024;
|
||||
unit += 1;
|
||||
}
|
||||
return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
|
||||
}
|
||||
|
||||
const htmlEscape = window.WarpBoxUI.htmlEscape;
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function totalBytes() {
|
||||
return files.reduce((sum, item) => sum + item.file.size, 0);
|
||||
}
|
||||
|
||||
function uploadedBytes() {
|
||||
return files.reduce((sum, item) => sum + item.loaded, 0);
|
||||
}
|
||||
|
||||
function overallProgress() {
|
||||
const total = totalBytes();
|
||||
return total ? Math.round((uploadedBytes() / total) * 100) : 0;
|
||||
}
|
||||
|
||||
function oversizedFiles() {
|
||||
return maxFileBytes ? files.filter((item) => item.file.size > maxFileBytes) : [];
|
||||
}
|
||||
|
||||
function isOverBoxQuota() {
|
||||
return maxBoxBytes ? totalBytes() > maxBoxBytes : false;
|
||||
}
|
||||
|
||||
function hasQuotaError() {
|
||||
return isOverBoxQuota() || oversizedFiles().length > 0;
|
||||
}
|
||||
|
||||
function normalizedFileName(name) {
|
||||
return String(name || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function splitNameForIncrement(name) {
|
||||
const value = String(name || "file");
|
||||
const dot = value.lastIndexOf(".");
|
||||
if (dot > 0 && dot < value.length - 1) return [value.slice(0, dot), value.slice(dot)];
|
||||
return [value, ""];
|
||||
}
|
||||
|
||||
function nextIncrementedFileName(name, usedNames) {
|
||||
const [base, ext] = splitNameForIncrement(name);
|
||||
let index = 2;
|
||||
let candidate = `${base} (${index})${ext}`;
|
||||
while (usedNames.has(normalizedFileName(candidate))) {
|
||||
index += 1;
|
||||
candidate = `${base} (${index})${ext}`;
|
||||
}
|
||||
usedNames.add(normalizedFileName(candidate));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function makeQueuedFile(file, displayName = file.name) {
|
||||
return {
|
||||
file,
|
||||
displayName,
|
||||
loaded: 0,
|
||||
uploaded: false,
|
||||
failed: false,
|
||||
error: "",
|
||||
row: null,
|
||||
boxID: "",
|
||||
boxFile: null,
|
||||
previewURL: file.type?.startsWith("image/") ? URL.createObjectURL(file) : "",
|
||||
};
|
||||
}
|
||||
|
||||
function iconForFile(file) {
|
||||
const filename = file.name || "";
|
||||
const mimeType = file.type || "";
|
||||
const extension = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
|
||||
|
||||
if (extension === ".exe") return "/static/img/icons/Program Files Icons - PNG/MSONSEXT.DLL_14_6-0.png";
|
||||
if (mimeType.startsWith("image/")) return "/static/img/sprites/bitmap.png";
|
||||
if (mimeType.startsWith("video/") || mimeType.startsWith("audio/")) return "/static/img/icons/netshow_notransm-1.png";
|
||||
if (mimeType.startsWith("text/") || extension === ".md") return "/static/img/sprites/notepad_file-1.png";
|
||||
if (mimeType.includes("zip") || mimeType.includes("compressed") || [".rar", ".7z", ".tar", ".gz"].includes(extension)) return "/static/img/icons/Windows Icons - PNG/zipfldr.dll_14_101-0.png";
|
||||
if ([".ttf", ".otf", ".woff", ".woff2"].includes(extension)) return "/static/img/sprites/font.png";
|
||||
if (extension === ".pdf") return "/static/img/sprites/journal.png";
|
||||
if ([".html", ".css", ".js"].includes(extension)) return "/static/img/sprites/frame_web-0.png";
|
||||
return "/static/img/icons/Windows Icons - PNG/ole2.dll_14_DEFICON.png";
|
||||
}
|
||||
22
static/js/upload/terminal.js
Normal file
22
static/js/upload/terminal.js
Normal file
@@ -0,0 +1,22 @@
|
||||
function getCurlCommand({ full = true } = {}) {
|
||||
const args = [];
|
||||
const selectedFiles = files.length ? files : [{ displayName: "build.zip" }];
|
||||
const previewLimit = full ? selectedFiles.length : 4;
|
||||
selectedFiles.slice(0, previewLimit).forEach((item) => args.push(` -F ${shellQuote(`files=@${item.displayName}`)}`));
|
||||
const hiddenFileCount = !full && selectedFiles.length > previewLimit ? selectedFiles.length - previewLimit : 0;
|
||||
args.push(` -F ${shellQuote(`retention=${el.expiry?.value || defaultRetention}`)}`);
|
||||
if (el.password?.value) args.push(` -F ${shellQuote("password=YOUR_PASSWORD")}`);
|
||||
if (el.allowZip && !el.allowZip.checked) args.push(` -F ${shellQuote("allow_zip=false")}`);
|
||||
|
||||
const commandLines = ["curl"];
|
||||
if (el.apiKeyMode?.checked) commandLines.push(` -H ${shellQuote("Authorization: Bearer YOUR_API_KEY")}`);
|
||||
commandLines.push(...args, ` ${window.location.origin}/upload`);
|
||||
const command = commandLines.join(" \\\n");
|
||||
return hiddenFileCount ? `${command}\n# and ${hiddenFileCount} other files included when copying` : command;
|
||||
}
|
||||
|
||||
function updateTerminal() {
|
||||
if (!el.terminal) return;
|
||||
const command = getCurlCommand({ full: false });
|
||||
el.terminal.innerHTML = `<span class="terminal-muted">warpbox@cli</span>:~$ ${htmlEscape(command)}`;
|
||||
}
|
||||
85
static/js/upload/upload-flow.js
Normal file
85
static/js/upload/upload-flow.js
Normal file
@@ -0,0 +1,85 @@
|
||||
async function startUpload() {
|
||||
if (!uploadsEnabled) {
|
||||
showToast("Guest uploads are disabled.", "warning");
|
||||
return;
|
||||
}
|
||||
if (uploadLocked) {
|
||||
showToast("Upload already started. Press Clear to create another box.", "warning");
|
||||
return;
|
||||
}
|
||||
if (!files.length) {
|
||||
showWarningDialog("No files selected", "There are no files selected. Please select files to upload.");
|
||||
showToast("No files selected. Please select files to upload.", "warning");
|
||||
setStatus("No files selected");
|
||||
return;
|
||||
}
|
||||
if (hasQuotaError()) {
|
||||
showWarningDialog("Over maximum upload size", quotaWarningMessage() || "Over maximum upload size.");
|
||||
showToast("Over maximum upload size.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
uploadLocked = true;
|
||||
setBoxOptionsLocked(true);
|
||||
if (el.fileInput) el.fileInput.disabled = true;
|
||||
el.dropzone?.classList.add("is-locked");
|
||||
setShareUrl("");
|
||||
files.forEach((item) => {
|
||||
item.loaded = 0;
|
||||
item.uploaded = false;
|
||||
item.failed = false;
|
||||
item.error = "";
|
||||
});
|
||||
completedImpactKeys = new Set();
|
||||
overallImpactDone = false;
|
||||
renderFiles();
|
||||
|
||||
let completedCount = 0;
|
||||
const totalCount = files.length;
|
||||
const statusPrefix = () => `${completedCount}/${totalCount}`;
|
||||
setStatus(`${statusPrefix()} Uploading.`);
|
||||
animateUploadStatus(statusPrefix);
|
||||
|
||||
try {
|
||||
const box = await createBox();
|
||||
setShareUrl(box.box_url);
|
||||
files.forEach((item, index) => {
|
||||
item.boxID = box.box_id;
|
||||
item.boxFile = box.files[index];
|
||||
item.displayName = item.boxFile?.name || item.displayName;
|
||||
const icon = item.row?.querySelector(".upload-file-icon");
|
||||
if (icon && item.boxFile?.thumbnail_path) {
|
||||
item.row.classList.add("has-thumbnail");
|
||||
icon.src = item.boxFile.thumbnail_path;
|
||||
} else if (icon && item.boxFile?.icon_path && !item.previewURL) {
|
||||
icon.src = item.boxFile.icon_path;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(files.map((item) => uploadFile(item, () => { completedCount += 1; })));
|
||||
stopStatusAnimation();
|
||||
|
||||
const failedCount = results.filter((result) => result.status === "rejected").length;
|
||||
if (failedCount > 0) {
|
||||
setStatus(`${completedCount}/${totalCount} uploaded, ${failedCount} failed`);
|
||||
showToast(`${failedCount} file${failedCount === 1 ? "" : "s"} failed. The share URL contains the successful files.`, "error");
|
||||
renderFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
setOverallProgress(100);
|
||||
setStatus(`${completedCount}/${totalCount} uploaded. Share URL created. Press Clear to start another upload.`);
|
||||
showToast("Upload complete. Share URL created.");
|
||||
renderFiles();
|
||||
} catch (error) {
|
||||
stopStatusAnimation();
|
||||
uploadLocked = false;
|
||||
setBoxOptionsLocked(false);
|
||||
if (el.fileInput) el.fileInput.disabled = !uploadsEnabled;
|
||||
el.dropzone?.classList.remove("is-locked");
|
||||
setShareUrl("");
|
||||
setStatus(error.message || "Upload failed");
|
||||
showToast(error.message || "Upload failed", "error");
|
||||
renderFiles();
|
||||
}
|
||||
}
|
||||
@@ -32,17 +32,26 @@ window.WarpBoxUI = (() => {
|
||||
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 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]) : "";
|
||||
});
|
||||
}
|
||||
function htmlEscape(value) {
|
||||
return String(value || "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
return { toast, openPopup, closePopup, renderTemplate };
|
||||
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, htmlEscape, renderTemplate };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user