fix: bugs in general
This commit is contained in:
@@ -109,7 +109,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: repeating-linear-gradient(45deg, #c0c0c0 0 2px, #b5b5b5 2px 4px);
|
background: repeating-linear-gradient(45deg, #808080 0 2px, #8f8f8f 2px 4px);
|
||||||
border-top: 1px solid #808080;
|
border-top: 1px solid #808080;
|
||||||
border-left: 1px solid #808080;
|
border-left: 1px solid #808080;
|
||||||
border-right: 1px solid #ffffff;
|
border-right: 1px solid #ffffff;
|
||||||
@@ -126,8 +126,57 @@ textarea {
|
|||||||
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
|
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:single-button {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
background-color: #c0c0c0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 7px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:single-button:vertical:decrement {
|
||||||
|
background-image: linear-gradient(45deg, transparent 50%, #000000 50%), linear-gradient(135deg, #000000 50%, transparent 50%);
|
||||||
|
background-position: 5px 6px, 8px 6px;
|
||||||
|
background-size: 4px 4px, 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:single-button:vertical:increment {
|
||||||
|
background-image: linear-gradient(225deg, transparent 50%, #000000 50%), linear-gradient(315deg, #000000 50%, transparent 50%);
|
||||||
|
background-position: 5px 7px, 8px 7px;
|
||||||
|
background-size: 4px 4px, 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:single-button:horizontal:decrement {
|
||||||
|
background-image: linear-gradient(135deg, transparent 50%, #000000 50%), linear-gradient(45deg, #000000 50%, transparent 50%);
|
||||||
|
background-position: 6px 5px, 6px 8px;
|
||||||
|
background-size: 4px 4px, 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:single-button:horizontal:increment {
|
||||||
|
background-image: linear-gradient(315deg, transparent 50%, #000000 50%), linear-gradient(225deg, #000000 50%, transparent 50%);
|
||||||
|
background-position: 7px 5px, 7px 8px;
|
||||||
|
background-size: 4px 4px, 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover,
|
||||||
|
::-webkit-scrollbar-button:single-button:hover {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:active,
|
||||||
|
::-webkit-scrollbar-button:single-button:active {
|
||||||
|
border-top-color: #000000;
|
||||||
|
border-left-color: #000000;
|
||||||
|
border-right-color: #ffffff;
|
||||||
|
border-bottom-color: #ffffff;
|
||||||
|
box-shadow: inset -1px -1px 0 #dfdfdf, inset 1px 1px 0 #808080;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
::-webkit-scrollbar-corner {
|
||||||
background: #c0c0c0;
|
background: #c0c0c0;
|
||||||
|
border-top: 1px solid #808080;
|
||||||
|
border-left: 1px solid #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.win98-button {
|
.win98-button {
|
||||||
@@ -231,10 +280,12 @@ textarea:disabled {
|
|||||||
.popup-body li { margin: 0 0 4px; }
|
.popup-body li { margin: 0 0 4px; }
|
||||||
.popup-body .code-block {
|
.popup-body .code-block {
|
||||||
margin: 6px 0 10px;
|
margin: 6px 0 10px;
|
||||||
padding: 8px 8px 22px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding: 8px;
|
||||||
color: #00ff66;
|
color: #00ff66;
|
||||||
background: #000000;
|
background: #000000;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -243,13 +294,20 @@ textarea:disabled {
|
|||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-body .code-block::after {
|
.popup-body .code-block code {
|
||||||
content: "\A";
|
display: inline-block;
|
||||||
white-space: pre;
|
min-width: 100%;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
white-space: inherit;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-fallback-text {
|
.copy-fallback-text {
|
||||||
|
|||||||
@@ -284,6 +284,14 @@ body.fit-window .box-window {
|
|||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-frame.is-text code {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100%;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
white-space: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.box-empty {
|
.box-empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
@@ -775,7 +775,7 @@ body.fit-window .desktop-wrap {
|
|||||||
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
white-space: pre;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-box::after {
|
.terminal-box::after {
|
||||||
@@ -910,10 +910,12 @@ body.fit-window .desktop-wrap {
|
|||||||
.popup-body li { margin: 0 0 4px; }
|
.popup-body li { margin: 0 0 4px; }
|
||||||
.popup-body .code-block {
|
.popup-body .code-block {
|
||||||
margin: 6px 0 10px;
|
margin: 6px 0 10px;
|
||||||
padding: 8px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding: 8px;
|
||||||
color: #00ff66;
|
color: #00ff66;
|
||||||
background: #000000;
|
background: #000000;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -921,7 +923,10 @@ body.fit-window .desktop-wrap {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-window.is-about-popup .popup-body {
|
.popup-window.is-about-popup .popup-body {
|
||||||
@@ -1064,13 +1069,18 @@ body.fit-window .desktop-wrap {
|
|||||||
|
|
||||||
.popup-body .code-block {
|
.popup-body .code-block {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
padding-bottom: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-body .code-block::after {
|
.popup-body .code-block code {
|
||||||
content: "\A";
|
display: inline-block;
|
||||||
white-space: pre;
|
min-width: 100%;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
white-space: inherit;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kbd {
|
.kbd {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const toast = document.querySelector("#toast");
|
|||||||
const zipOnly = boxPanel && boxPanel.dataset.zipOnly === "true";
|
const zipOnly = boxPanel && boxPanel.dataset.zipOnly === "true";
|
||||||
|
|
||||||
let contextFile = null;
|
let contextFile = null;
|
||||||
|
let lastStatusSignature = "";
|
||||||
|
|
||||||
function htmlEscape(value) {
|
function htmlEscape(value) {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
@@ -149,7 +150,7 @@ async function previewFile(item) {
|
|||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error("Preview failed");
|
if (!response.ok) throw new Error("Preview failed");
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
openPopup(`${data.name} preview`, `<code class="code-block preview-frame is-text">${htmlEscape(text.slice(0, 120000))}</code>`, { preview: true });
|
openPopup(`${data.name} preview`, `<pre class="code-block preview-frame is-text"><code>${htmlEscape(text.slice(0, 120000))}</code></pre>`, { preview: true });
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
showToast("The browser could not load a text preview.", "error");
|
showToast("The browser could not load a text preview.", "error");
|
||||||
}
|
}
|
||||||
@@ -214,9 +215,13 @@ async function refreshBoxStatus() {
|
|||||||
|
|
||||||
const boxID = boxPanel.dataset.boxId;
|
const boxID = boxPanel.dataset.boxId;
|
||||||
const response = await fetch(`/box/${boxID}/status`);
|
const response = await fetch(`/box/${boxID}/status`);
|
||||||
if (!response.ok) return true;
|
if (!response.ok) return { changed: false, hasLoadingFiles: true };
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
const signature = statusSignature(result);
|
||||||
|
const changed = signature !== lastStatusSignature;
|
||||||
|
lastStatusSignature = signature;
|
||||||
|
|
||||||
if (boxExpiryMeta && typeof result.expires_at === "string") {
|
if (boxExpiryMeta && typeof result.expires_at === "string") {
|
||||||
boxExpiryMeta.dataset.expiresAt = result.expires_at;
|
boxExpiryMeta.dataset.expiresAt = result.expires_at;
|
||||||
updateExpiryCountdown();
|
updateExpiryCountdown();
|
||||||
@@ -228,11 +233,73 @@ async function refreshBoxStatus() {
|
|||||||
boxStatus.textContent = `${completeCount}/${result.files.length} ready`;
|
boxStatus.textContent = `${completeCount}/${result.files.length} ready`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.files.some((file) => {
|
const hasLoadingFiles = result.files.some((file) => {
|
||||||
const isUploading = file.status === "pending" || file.status === "uploading";
|
const isUploading = file.status === "pending" || file.status === "uploading";
|
||||||
const isWaitingForThumbnail = file.status === "complete" && !file.thumbnail_status && !file.thumbnail_path;
|
const isWaitingForThumbnail = file.status === "complete" && !file.thumbnail_status && !file.thumbnail_path;
|
||||||
return isUploading || isWaitingForThumbnail || file.thumbnail_status === "processing";
|
return isUploading || isWaitingForThumbnail || file.thumbnail_status === "processing";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { changed, hasLoadingFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusSignature(result) {
|
||||||
|
const files = Array.isArray(result.files) ? result.files : [];
|
||||||
|
return JSON.stringify({
|
||||||
|
expiresAt: result.expires_at || "",
|
||||||
|
files: files.map((file) => ({
|
||||||
|
id: file.id,
|
||||||
|
status: file.status,
|
||||||
|
size: file.size,
|
||||||
|
thumbnailPath: file.thumbnail_path || "",
|
||||||
|
thumbnailStatus: file.thumbnail_status || "",
|
||||||
|
downloadPath: file.download_path || "",
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollingStages(baseMS) {
|
||||||
|
return [
|
||||||
|
{ interval: baseMS, attempts: 10 },
|
||||||
|
{ interval: baseMS * 2, attempts: 20 },
|
||||||
|
{ interval: baseMS * 10, attempts: 100 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function startStagedPolling(baseMS) {
|
||||||
|
const stages = pollingStages(baseMS);
|
||||||
|
let stageIndex = 0;
|
||||||
|
let attemptsInStage = 0;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
const tick = async () => {
|
||||||
|
if (stopped) return;
|
||||||
|
const stage = stages[stageIndex];
|
||||||
|
try {
|
||||||
|
const result = await refreshBoxStatus();
|
||||||
|
if (result.changed) {
|
||||||
|
stageIndex = 0;
|
||||||
|
attemptsInStage = 0;
|
||||||
|
} else {
|
||||||
|
attemptsInStage += 1;
|
||||||
|
if (attemptsInStage >= stage.attempts) {
|
||||||
|
stageIndex += 1;
|
||||||
|
attemptsInStage = 0;
|
||||||
|
if (stageIndex >= stages.length) {
|
||||||
|
stopped = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
attemptsInStage += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stopped) {
|
||||||
|
window.setTimeout(tick, stages[stageIndex].interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setTimeout(tick, stages[0].interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
@@ -296,12 +363,5 @@ setInterval(updateExpiryCountdown, 1000);
|
|||||||
|
|
||||||
if (boxPanel) {
|
if (boxPanel) {
|
||||||
const pollMS = Number.parseInt(boxPanel.dataset.pollMs, 10) || 5000;
|
const pollMS = Number.parseInt(boxPanel.dataset.pollMs, 10) || 5000;
|
||||||
const timer = setInterval(async () => {
|
startStagedPolling(pollMS);
|
||||||
try {
|
|
||||||
const hasLoadingFiles = await refreshBoxStatus();
|
|
||||||
if (!hasLoadingFiles) clearInterval(timer);
|
|
||||||
} catch (_) {
|
|
||||||
// Keep polling through temporary network/server hiccups.
|
|
||||||
}
|
|
||||||
}, pollMS);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<h3>Upload with cURL</h3>
|
<h3>Upload from a terminal</h3>
|
||||||
<p>WarpBox accepts normal multipart form uploads through the compatibility endpoint:</p>
|
<p>WarpBox accepts normal multipart uploads at <code>/upload</code>. The server returns JSON with a <code>box_url</code> you can open or share.</p>
|
||||||
<code class="code-block">curl \
|
<pre class="code-block"><code>curl \
|
||||||
-F 'files=@./my-file.zip' \
|
-F 'files=@./my-file.zip' \
|
||||||
-F 'retention=1h' \
|
-F 'retention=1h' \
|
||||||
{{ origin }}/upload
|
{{ origin }}/upload
|
||||||
</code>
|
</code></pre>
|
||||||
<h4>Browser flow</h4>
|
|
||||||
<p>The browser uses the manifest API: it creates a box, uploads each file, and marks failed uploads so the download page does not wait forever.</p>
|
<h4>Reusable shell wrapper</h4>
|
||||||
<h4>Make a WarpBox executable</h4>
|
<p>This version is small, portable, and works well as a personal <code>warpbox</code> command.</p>
|
||||||
<p>Save this as <code>warpbox</code>, make it executable, and put it somewhere on your <code>PATH</code>.</p>
|
<pre class="code-block"><code>#!/usr/bin/env bash
|
||||||
<code class="code-block">#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if [ "$#" -lt 1 ]; then
|
if [ "$#" -lt 1 ]; then
|
||||||
@@ -25,9 +24,39 @@ for file in "$@"; do
|
|||||||
args+=(-F "files=@${file}")
|
args+=(-F "files=@${file}")
|
||||||
done
|
done
|
||||||
|
|
||||||
curl "${args[@]}" "${endpoint}"
|
curl --fail-with-body "${args[@]}" "${endpoint}"
|
||||||
</code>
|
</code></pre>
|
||||||
<code class="code-block">chmod +x ./warpbox
|
|
||||||
|
<h4>Install it</h4>
|
||||||
|
<p>Put the wrapper somewhere on your <code>PATH</code>, then call it with one or more files.</p>
|
||||||
|
<pre class="code-block"><code>chmod +x ./warpbox
|
||||||
sudo install -m 755 ./warpbox /usr/local/bin/warpbox
|
sudo install -m 755 ./warpbox /usr/local/bin/warpbox
|
||||||
warpbox ./my-file.zip
|
warpbox ./photo.png ./archive.zip
|
||||||
</code>
|
</code></pre>
|
||||||
|
|
||||||
|
<h4>Print only the share URL</h4>
|
||||||
|
<p>If <code>jq</code> is installed, this variant extracts the returned link and expands it to a full URL.</p>
|
||||||
|
<pre class="code-block"><code>warpbox() {
|
||||||
|
local endpoint="${WARPBOX_URL:-{{ origin }}}/upload"
|
||||||
|
local retention="${WARPBOX_RETENTION:-1h}"
|
||||||
|
local args=(-F "retention=${retention}")
|
||||||
|
|
||||||
|
for file in "$@"; do
|
||||||
|
args+=(-F "files=@${file}")
|
||||||
|
done
|
||||||
|
|
||||||
|
curl --fail-with-body -sS "${args[@]}" "${endpoint}" |
|
||||||
|
jq -r --arg origin "${WARPBOX_URL:-{{ origin }}}" '"\($origin)\(.box_url)"'
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<h4>Add password or retention</h4>
|
||||||
|
<p>You can keep the wrapper simple and pass fixed options through environment variables or extra form fields.</p>
|
||||||
|
<pre class="code-block"><code>WARPBOX_RETENTION=24h warpbox ./release.tar.gz
|
||||||
|
|
||||||
|
curl \
|
||||||
|
-F 'files=@./private.zip' \
|
||||||
|
-F 'retention=1h' \
|
||||||
|
-F 'password=correct-horse-battery-staple' \
|
||||||
|
{{ origin }}/upload
|
||||||
|
</code></pre>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<h3>Upload examples</h3>
|
<h3>Upload examples</h3>
|
||||||
<h4>Basic CLI upload</h4>
|
<h4>Basic CLI upload</h4>
|
||||||
<code class="code-block">curl \
|
<pre class="code-block"><code>curl \
|
||||||
-F 'files=@./photo.png' \
|
-F 'files=@./photo.png' \
|
||||||
-F 'retention=24h' \
|
-F 'retention=24h' \
|
||||||
{{ origin }}/upload
|
{{ origin }}/upload
|
||||||
</code>
|
</code></pre>
|
||||||
<h4>Multiple files with password</h4>
|
<h4>Multiple files with password</h4>
|
||||||
<code class="code-block">curl \
|
<pre class="code-block"><code>curl \
|
||||||
-F 'files=@./one.png' \
|
-F 'files=@./one.png' \
|
||||||
-F 'files=@./two.zip' \
|
-F 'files=@./two.zip' \
|
||||||
-F 'retention=1h' \
|
-F 'retention=1h' \
|
||||||
-F 'password=secret-pass' \
|
-F 'password=secret-pass' \
|
||||||
{{ origin }}/upload
|
{{ origin }}/upload
|
||||||
</code>
|
</code></pre>
|
||||||
<h4>Go</h4>
|
<h4>Go</h4>
|
||||||
<code class="code-block">package main
|
<pre class="code-block"><code>package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -48,9 +48,9 @@ func main() {
|
|||||||
out, _ := io.ReadAll(resp.Body)
|
out, _ := io.ReadAll(resp.Body)
|
||||||
fmt.Println(string(out))
|
fmt.Println(string(out))
|
||||||
}
|
}
|
||||||
</code>
|
</code></pre>
|
||||||
<h4>Java 11+ HttpClient</h4>
|
<h4>Java 11+ HttpClient</h4>
|
||||||
<code class="code-block">import java.net.URI;
|
<pre class="code-block"><code>import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
@@ -84,9 +84,9 @@ public class UploadWarpBox {
|
|||||||
System.out.println(response.body());
|
System.out.println(response.body());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</code>
|
</code></pre>
|
||||||
<h4>JavaScript Node.js</h4>
|
<h4>JavaScript Node.js</h4>
|
||||||
<code class="code-block">import { openAsBlob } from 'node:fs';
|
<pre class="code-block"><code>import { openAsBlob } from 'node:fs';
|
||||||
|
|
||||||
const file = await openAsBlob('./photo.png');
|
const file = await openAsBlob('./photo.png');
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
@@ -99,4 +99,4 @@ const res = await fetch('{{ origin }}/upload', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log(await res.text());
|
console.log(await res.text());
|
||||||
</code>
|
</code></pre>
|
||||||
|
|||||||
Reference in New Issue
Block a user