fix: bugs in general

This commit is contained in:
2026-04-29 02:35:23 +03:00
parent e330fb04b3
commit fb80f11e72
6 changed files with 211 additions and 46 deletions

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);
} }

View File

@@ -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>

View File

@@ -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>