Add status progress for batch tool operations

This commit is contained in:
2026-05-29 13:40:17 +08:00
parent 5625d94106
commit 80f155dfe8
14 changed files with 307 additions and 65 deletions

View File

@@ -167,6 +167,24 @@ function addFiles(files) {
selectedPage = Math.max(0, Math.ceil(selectedFiles.length / PAGE_SIZE) - 1);
renderSelection();
}
async function readJSONStream(response, handle) {
if (!response.ok || !response.body) throw new Error('Request failed.');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newline;
while ((newline = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, newline);
buffer = buffer.slice(newline + 1);
if (line.trim()) handle(JSON.parse(line));
}
}
if (buffer.trim()) handle(JSON.parse(buffer));
}
renderSelection();
setupDropZone('dropZone', statusEl, (j) => {
addFiles(j.files || []);
@@ -201,11 +219,29 @@ startBtn.addEventListener('click', async () => {
startBtn.disabled = true;
statusEl.textContent = 'Starting server...';
statusEl.classList.add('is-busy');
const r = await fetch('/api/mobile/start', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ paths: selectedPaths }),
});
const j = await r.json();
let j = null;
try {
const r = await fetch('/api/mobile/start', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ paths: selectedPaths }),
});
await readJSONStream(r, (msg) => {
if (msg.type === 'status') {
statusEl.textContent = msg.message;
statusEl.classList.add('is-busy');
} else if (msg.type === 'done') {
j = msg;
} else if (msg.type === 'error') {
j = { error: msg.message };
}
});
} catch (e) {
statusEl.classList.remove('is-busy');
statusEl.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
startBtn.disabled = false;
return;
}
if (!j) { statusEl.classList.remove('is-busy'); statusEl.innerHTML = '<span class="err">No response.</span>'; startBtn.disabled = false; return; }
if (j.error) {
statusEl.classList.remove('is-busy');
statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
@@ -303,13 +339,14 @@ func MobilePickFolder(w http.ResponseWriter, r *http.Request) {
func MobileStart(w http.ResponseWriter, r *http.Request) {
stopActiveShare()
send := newJSONStream(w)
var req struct {
Path string `json:"path"`
Paths []string `json:"paths"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, map[string]any{"error": err.Error()})
send(map[string]any{"type": "error", "message": err.Error()})
return
}
paths := req.Paths
@@ -317,18 +354,19 @@ func MobileStart(w http.ResponseWriter, r *http.Request) {
paths = []string{req.Path}
}
if len(paths) == 0 {
writeJSON(w, map[string]any{"error": "File missing."})
send(map[string]any{"type": "error", "message": "File missing."})
return
}
files := make([]sharedFile, 0, len(paths))
for _, p := range paths {
for i, p := range paths {
if p == "" || !fileExists(p) {
writeJSON(w, map[string]any{"error": "File missing."})
send(map[string]any{"type": "error", "message": "File missing."})
return
}
send(map[string]any{"type": "status", "message": fmt.Sprintf("Loading %d/%d %s", i+1, len(paths), filepath.Base(p))})
fileBuf, err := os.ReadFile(p)
if err != nil {
writeJSON(w, map[string]any{"error": "Read failed: " + err.Error()})
send(map[string]any{"type": "error", "message": "Read failed: " + err.Error()})
return
}
files = append(files, sharedFile{buf: fileBuf, filename: filepath.Base(p)})
@@ -337,15 +375,16 @@ func MobileStart(w http.ResponseWriter, r *http.Request) {
// 6 random bytes -> base64url, no padding
tokenBytes := make([]byte, 6)
if _, err := rand.Read(tokenBytes); err != nil {
writeJSON(w, map[string]any{"error": "Token gen failed: " + err.Error()})
send(map[string]any{"type": "error", "message": "Token gen failed: " + err.Error()})
return
}
token := strings.TrimRight(base64.URLEncoding.EncodeToString(tokenBytes), "=")
cfgPort := LoadConfig().SendToMobile.Port
send(map[string]any{"type": "status", "message": "Starting server..."})
listener, err := net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(cfgPort))
if err != nil {
writeJSON(w, map[string]any{"error": "Failed to bind: " + err.Error()})
send(map[string]any{"type": "error", "message": "Failed to bind: " + err.Error()})
return
}
port := listener.Addr().(*net.TCPAddr).Port
@@ -363,7 +402,7 @@ func MobileStart(w http.ResponseWriter, r *http.Request) {
ips := getLanIPs()
if len(ips) == 0 {
_ = share.server.Close()
writeJSON(w, map[string]any{"error": "No non-loopback IPv4 interface found. Are you on a network?"})
send(map[string]any{"type": "error", "message": "No non-loopback IPv4 interface found. Are you on a network?"})
return
}
@@ -388,7 +427,7 @@ func MobileStart(w http.ResponseWriter, r *http.Request) {
if len(files) == 1 {
filename = files[0].filename
}
writeJSON(w, map[string]any{"urls": urls, "filename": filename})
send(map[string]any{"type": "done", "urls": urls, "filename": filename})
}
func MobileStop(w http.ResponseWriter, r *http.Request) {