Add status progress for batch tool operations
This commit is contained in:
@@ -134,11 +134,11 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
|||||||
if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; }
|
if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; }
|
||||||
|
|
||||||
if (i > 0 && delayMs > 0) {
|
if (i > 0 && delayMs > 0) {
|
||||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Waiting ${delayMs}ms before ${fileName}...` });
|
panel.webview.postMessage({ type: 'status', message: `Waiting ${idx}/${paths.length} ${fileName}` });
|
||||||
await sleep(delayMs);
|
await sleep(delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` });
|
panel.webview.postMessage({ type: 'status', message: `Validating ${idx}/${paths.length} ${fileName}` });
|
||||||
|
|
||||||
let res: Response;
|
let res: Response;
|
||||||
try {
|
try {
|
||||||
@@ -154,7 +154,7 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
|||||||
reportError(`Validation failed: ${validateText.slice(0, 300)}`); continue;
|
reportError(`Validation failed: ${validateText.slice(0, 300)}`); continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Uploading ${fileName}...` });
|
panel.webview.postMessage({ type: 'status', message: `Uploading ${idx}/${paths.length} ${fileName}` });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res = await fetch('https://p.applov.in/getCachedAdURL', {
|
res = await fetch('https://p.applov.in/getCachedAdURL', {
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ export function openBase64Scanner(_context: vscode.ExtensionContext) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const results: { file: string; ok: boolean; assets: string[] }[] = [];
|
const results: { file: string; ok: boolean; assets: string[] }[] = [];
|
||||||
for (const file of targets) {
|
for (let i = 0; i < targets.length; i++) {
|
||||||
|
const file = targets[i];
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Scanning ${i + 1}/${targets.length} ${path.basename(file)}` });
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
const offenders = findNonBase64Assets(content);
|
const offenders = findNonBase64Assets(content);
|
||||||
@@ -312,6 +314,9 @@ ${getToolWebviewStyles()}
|
|||||||
pickedFolder = '';
|
pickedFolder = '';
|
||||||
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
||||||
renderSelection();
|
renderSelection();
|
||||||
|
} else if (msg.type === 'status') {
|
||||||
|
statusEl.textContent = msg.message;
|
||||||
|
statusEl.className = 'status-panel is-busy';
|
||||||
} else if (msg.type === 'results') {
|
} else if (msg.type === 'results') {
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
statusEl.classList.remove('is-busy');
|
statusEl.classList.remove('is-busy');
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ export function openMraidChecker(_context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results: MraidResult[] = [];
|
const results: MraidResult[] = [];
|
||||||
for (const file of targets) {
|
for (let i = 0; i < targets.length; i++) {
|
||||||
|
const file = targets[i];
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Scanning ${i + 1}/${targets.length} ${path.basename(file)}` });
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
const issues = checkMraid(content, file);
|
const issues = checkMraid(content, file);
|
||||||
@@ -829,6 +831,9 @@ ${getToolWebviewStyles()}
|
|||||||
pickedFolder = '';
|
pickedFolder = '';
|
||||||
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
||||||
renderSelection();
|
renderSelection();
|
||||||
|
} else if (msg.type === 'status') {
|
||||||
|
statusEl.textContent = msg.message;
|
||||||
|
statusEl.className = 'status-panel is-busy';
|
||||||
} else if (msg.type === 'results') {
|
} else if (msg.type === 'results') {
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
statusEl.classList.remove('is-busy');
|
statusEl.classList.remove('is-busy');
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function openPlayworksConverter(_context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
let html: string;
|
let html: string;
|
||||||
try {
|
try {
|
||||||
|
panel.webview.postMessage({ type: 'status', message: 'Reading source HTML...' });
|
||||||
html = fs.readFileSync(opts.sourcePath, 'utf8');
|
html = fs.readFileSync(opts.sourcePath, 'utf8');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
panel.webview.postMessage({ type: 'convertDone', results: [], error: `Could not read source: ${e.message}` });
|
panel.webview.postMessage({ type: 'convertDone', results: [], error: `Could not read source: ${e.message}` });
|
||||||
@@ -76,11 +77,14 @@ export function openPlayworksConverter(_context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
const results: NetworkResult[] = [];
|
const results: NetworkResult[] = [];
|
||||||
try {
|
try {
|
||||||
|
panel.webview.postMessage({ type: 'status', message: 'Converting Unity...' });
|
||||||
results.push({ network: 'un', file: copyUnityInput(opts), ok: true });
|
results.push({ network: 'un', file: copyUnityInput(opts), ok: true });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
results.push({ network: 'un', file: '', ok: false, error: e.message });
|
results.push({ network: 'un', file: '', ok: false, error: e.message });
|
||||||
}
|
}
|
||||||
for (const network of opts.networks) {
|
for (let i = 0; i < opts.networks.length; i++) {
|
||||||
|
const network = opts.networks[i];
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Converting ${i + 1}/${opts.networks.length} ${network}` });
|
||||||
try {
|
try {
|
||||||
const filePath = await convertNetwork(html, opts, network);
|
const filePath = await convertNetwork(html, opts, network);
|
||||||
results.push({ network, file: filePath, ok: true });
|
results.push({ network, file: filePath, ok: true });
|
||||||
@@ -600,6 +604,9 @@ ${getToolWebviewStyles()}
|
|||||||
outEl.value = msg.path;
|
outEl.value = msg.path;
|
||||||
outputDir = msg.path;
|
outputDir = msg.path;
|
||||||
updateConvertBtn();
|
updateConvertBtn();
|
||||||
|
} else if (msg.type === 'status') {
|
||||||
|
statusEl.textContent = msg.message;
|
||||||
|
statusEl.className = 'status-panel is-busy';
|
||||||
} else if (msg.type === 'convertDone') {
|
} else if (msg.type === 'convertDone') {
|
||||||
convertBtn.disabled = false;
|
convertBtn.disabled = false;
|
||||||
updateConvertBtn();
|
updateConvertBtn();
|
||||||
|
|||||||
@@ -101,11 +101,13 @@ async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) {
|
|||||||
for (let i = 0; i < valid.length; i++) {
|
for (let i = 0; i < valid.length; i++) {
|
||||||
const r = valid[i];
|
const r = valid[i];
|
||||||
const idx = i + 1;
|
const idx = i + 1;
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Preparing ${idx}/${valid.length} ${path.basename(r.path)}` });
|
||||||
const buf = fs.readFileSync(r.path);
|
const buf = fs.readFileSync(r.path);
|
||||||
const file = new File([buf], stampedNames[i], { type: 'text/html' });
|
const file = new File([buf], stampedNames[i], { type: 'text/html' });
|
||||||
form.append(`fileUpload_${idx}`, file as any);
|
form.append(`fileUpload_${idx}`, file as any);
|
||||||
form.append(`iterationNameUpload_${idx}`, encodeURI(r.iteration.replace(/\s+/g, '')));
|
form.append(`iterationNameUpload_${idx}`, encodeURI(r.iteration.replace(/\s+/g, '')));
|
||||||
}
|
}
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Uploading ${valid.length} file${valid.length === 1 ? '' : 's'}...` });
|
||||||
|
|
||||||
let res: Response;
|
let res: Response;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -103,13 +103,16 @@ export function openSendToMobile(context: vscode.ExtensionContext) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const files: SharedFile[] = [];
|
const files: SharedFile[] = [];
|
||||||
for (const filePath of filePaths) {
|
for (let i = 0; i < filePaths.length; i++) {
|
||||||
|
const filePath = filePaths[i];
|
||||||
if (!filePath || !fs.existsSync(filePath)) {
|
if (!filePath || !fs.existsSync(filePath)) {
|
||||||
panel.webview.postMessage({ type: 'error', message: 'File missing.' });
|
panel.webview.postMessage({ type: 'error', message: 'File missing.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Loading ${i + 1}/${filePaths.length} ${path.basename(filePath)}` });
|
||||||
files.push({ buf: fs.readFileSync(filePath), filename: path.basename(filePath) });
|
files.push({ buf: fs.readFileSync(filePath), filename: path.basename(filePath) });
|
||||||
}
|
}
|
||||||
|
panel.webview.postMessage({ type: 'status', message: 'Starting server...' });
|
||||||
const token = crypto.randomBytes(6).toString('base64url');
|
const token = crypto.randomBytes(6).toString('base64url');
|
||||||
|
|
||||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.sendToMobile');
|
const cfg = vscode.workspace.getConfiguration('hplToolbox.sendToMobile');
|
||||||
@@ -549,6 +552,9 @@ ${getToolWebviewStyles()}
|
|||||||
selectedPaths = selectedFiles.map(f => f.path);
|
selectedPaths = selectedFiles.map(f => f.path);
|
||||||
selectedPage = 0;
|
selectedPage = 0;
|
||||||
renderSelection();
|
renderSelection();
|
||||||
|
} else if (m.type === 'status') {
|
||||||
|
statusEl.textContent = m.message;
|
||||||
|
statusEl.className = 'status-panel is-busy';
|
||||||
} else if (m.type === 'sharing') {
|
} else if (m.type === 'sharing') {
|
||||||
statusEl.classList.remove('is-busy');
|
statusEl.classList.remove('is-busy');
|
||||||
currentUrls = m.urls;
|
currentUrls = m.urls;
|
||||||
|
|||||||
@@ -451,11 +451,11 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if i > 0 && delayMs > 0 {
|
if i > 0 && delayMs > 0 {
|
||||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Waiting %dms before %s...", idx, len(req.Paths), delayMs, fileName)})
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Waiting %d/%d %s", idx, len(req.Paths), fileName)})
|
||||||
time.Sleep(time.Duration(delayMs) * time.Millisecond)
|
time.Sleep(time.Duration(delayMs) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Validating %s...", idx, len(req.Paths), fileName)})
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Validating %d/%d %s", idx, len(req.Paths), fileName)})
|
||||||
|
|
||||||
body, ct, err := buildForm(filePath)
|
body, ct, err := buildForm(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -480,7 +480,7 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Uploading %s...", idx, len(req.Paths), fileName)})
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d/%d %s", idx, len(req.Paths), fileName)})
|
||||||
|
|
||||||
body, ct, err = buildForm(filePath)
|
body, ct, err = buildForm(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -89,6 +90,24 @@ function addPaths(paths) {
|
|||||||
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
||||||
renderSelection();
|
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();
|
renderSelection();
|
||||||
setupDropZone('dropZone', statusEl, (j) => {
|
setupDropZone('dropZone', statusEl, (j) => {
|
||||||
addPaths(j.paths || []);
|
addPaths(j.paths || []);
|
||||||
@@ -119,8 +138,25 @@ document.getElementById('scan').addEventListener('click', async () => {
|
|||||||
resultsEl.innerHTML = '';
|
resultsEl.innerHTML = '';
|
||||||
if (!pickedFiles.length) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Pick files first.'; return; }
|
if (!pickedFiles.length) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Pick files first.'; return; }
|
||||||
const payload = { files: pickedFiles };
|
const payload = { files: pickedFiles };
|
||||||
const r = await fetch('/api/base64/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
let j = null;
|
||||||
const j = await r.json();
|
try {
|
||||||
|
const r = await fetch('/api/base64/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||||
|
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.textContent = 'Error: ' + (e.message || e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!j) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: No response.'; return; }
|
||||||
if (j.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + j.error; return; }
|
if (j.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + j.error; return; }
|
||||||
const total = j.results.length;
|
const total = j.results.length;
|
||||||
const flagged = j.results.filter(r => !r.ok).length;
|
const flagged = j.results.filter(r => !r.ok).length;
|
||||||
@@ -168,12 +204,13 @@ type scanResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Base64Scan(w http.ResponseWriter, r *http.Request) {
|
func Base64Scan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
send := newJSONStream(w)
|
||||||
var req struct {
|
var req struct {
|
||||||
Folder string `json:"folder"`
|
Folder string `json:"folder"`
|
||||||
Files []string `json:"files"`
|
Files []string `json:"files"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeJSON(w, map[string]any{"results": []scanResult{}, "error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,12 +230,13 @@ func Base64Scan(w http.ResponseWriter, r *http.Request) {
|
|||||||
targets = collectHTMLFiles(req.Folder)
|
targets = collectHTMLFiles(req.Folder)
|
||||||
baseDir = req.Folder
|
baseDir = req.Folder
|
||||||
} else {
|
} else {
|
||||||
writeJSON(w, map[string]any{"results": []scanResult{}, "error": "Pick a folder or files first"})
|
send(map[string]any{"type": "error", "message": "Pick a folder or files first"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results := []scanResult{}
|
results := []scanResult{}
|
||||||
for _, file := range targets {
|
for i, file := range targets {
|
||||||
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Scanning %d/%d %s", i+1, len(targets), filepath.Base(file))})
|
||||||
data, err := os.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -219,7 +257,7 @@ func Base64Scan(w http.ResponseWriter, r *http.Request) {
|
|||||||
Assets: offenders,
|
Assets: offenders,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{"results": results})
|
send(map[string]any{"type": "done", "results": results})
|
||||||
}
|
}
|
||||||
|
|
||||||
func commonBaseDir(files []string) string {
|
func commonBaseDir(files []string) string {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,3 +10,27 @@ func writeJSON(w http.ResponseWriter, v any) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newJSONStream(w http.ResponseWriter) func(any) {
|
||||||
|
w.Header().Set("Content-Type", "application/x-ndjson; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
flusher, _ := w.(http.Flusher)
|
||||||
|
return func(v any) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
data = []byte(fmt.Sprintf(`{"type":"error","message":%q}`, err.Error()))
|
||||||
|
}
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
_, _ = w.Write([]byte("\n"))
|
||||||
|
if flusher != nil {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluralS(n int) string {
|
||||||
|
if n == 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "s"
|
||||||
|
}
|
||||||
|
|||||||
@@ -167,6 +167,24 @@ function addFiles(files) {
|
|||||||
selectedPage = Math.max(0, Math.ceil(selectedFiles.length / PAGE_SIZE) - 1);
|
selectedPage = Math.max(0, Math.ceil(selectedFiles.length / PAGE_SIZE) - 1);
|
||||||
renderSelection();
|
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();
|
renderSelection();
|
||||||
setupDropZone('dropZone', statusEl, (j) => {
|
setupDropZone('dropZone', statusEl, (j) => {
|
||||||
addFiles(j.files || []);
|
addFiles(j.files || []);
|
||||||
@@ -201,11 +219,29 @@ startBtn.addEventListener('click', async () => {
|
|||||||
startBtn.disabled = true;
|
startBtn.disabled = true;
|
||||||
statusEl.textContent = 'Starting server...';
|
statusEl.textContent = 'Starting server...';
|
||||||
statusEl.classList.add('is-busy');
|
statusEl.classList.add('is-busy');
|
||||||
const r = await fetch('/api/mobile/start', {
|
let j = null;
|
||||||
method:'POST', headers:{'Content-Type':'application/json'},
|
try {
|
||||||
body: JSON.stringify({ paths: selectedPaths }),
|
const r = await fetch('/api/mobile/start', {
|
||||||
});
|
method:'POST', headers:{'Content-Type':'application/json'},
|
||||||
const j = await r.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) {
|
if (j.error) {
|
||||||
statusEl.classList.remove('is-busy');
|
statusEl.classList.remove('is-busy');
|
||||||
statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
|
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) {
|
func MobileStart(w http.ResponseWriter, r *http.Request) {
|
||||||
stopActiveShare()
|
stopActiveShare()
|
||||||
|
send := newJSONStream(w)
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Paths []string `json:"paths"`
|
Paths []string `json:"paths"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
paths := req.Paths
|
paths := req.Paths
|
||||||
@@ -317,18 +354,19 @@ func MobileStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
paths = []string{req.Path}
|
paths = []string{req.Path}
|
||||||
}
|
}
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
writeJSON(w, map[string]any{"error": "File missing."})
|
send(map[string]any{"type": "error", "message": "File missing."})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
files := make([]sharedFile, 0, len(paths))
|
files := make([]sharedFile, 0, len(paths))
|
||||||
for _, p := range paths {
|
for i, p := range paths {
|
||||||
if p == "" || !fileExists(p) {
|
if p == "" || !fileExists(p) {
|
||||||
writeJSON(w, map[string]any{"error": "File missing."})
|
send(map[string]any{"type": "error", "message": "File missing."})
|
||||||
return
|
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)
|
fileBuf, err := os.ReadFile(p)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
files = append(files, sharedFile{buf: fileBuf, filename: filepath.Base(p)})
|
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
|
// 6 random bytes -> base64url, no padding
|
||||||
tokenBytes := make([]byte, 6)
|
tokenBytes := make([]byte, 6)
|
||||||
if _, err := rand.Read(tokenBytes); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
token := strings.TrimRight(base64.URLEncoding.EncodeToString(tokenBytes), "=")
|
token := strings.TrimRight(base64.URLEncoding.EncodeToString(tokenBytes), "=")
|
||||||
|
|
||||||
cfgPort := LoadConfig().SendToMobile.Port
|
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))
|
listener, err := net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(cfgPort))
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
port := listener.Addr().(*net.TCPAddr).Port
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
@@ -363,7 +402,7 @@ func MobileStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
ips := getLanIPs()
|
ips := getLanIPs()
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
_ = share.server.Close()
|
_ = 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +427,7 @@ func MobileStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
if len(files) == 1 {
|
if len(files) == 1 {
|
||||||
filename = files[0].filename
|
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) {
|
func MobileStop(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -131,6 +132,24 @@ function addPaths(paths) {
|
|||||||
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
|
||||||
renderSelection();
|
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();
|
renderSelection();
|
||||||
setupDropZone('dropZone', statusEl, (j) => {
|
setupDropZone('dropZone', statusEl, (j) => {
|
||||||
addPaths(j.paths || []);
|
addPaths(j.paths || []);
|
||||||
@@ -161,8 +180,25 @@ document.getElementById('scan').addEventListener('click', async () => {
|
|||||||
resultsEl.innerHTML = '';
|
resultsEl.innerHTML = '';
|
||||||
if (!pickedFiles.length) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Pick files first.'; return; }
|
if (!pickedFiles.length) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Pick files first.'; return; }
|
||||||
const payload = { files: pickedFiles };
|
const payload = { files: pickedFiles };
|
||||||
const r = await fetch('/api/mraid/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
let j = null;
|
||||||
const j = await r.json();
|
try {
|
||||||
|
const r = await fetch('/api/mraid/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||||
|
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, results: msg.results || [], skipped: msg.skipped, skippedReason: msg.skippedReason };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
statusEl.classList.remove('is-busy');
|
||||||
|
statusEl.textContent = 'Error: ' + (e.message || e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!j) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: No response.'; return; }
|
||||||
if (j.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + j.error; return; }
|
if (j.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + j.error; return; }
|
||||||
const total = j.results.length;
|
const total = j.results.length;
|
||||||
const withIssues = j.results.filter(r => !r.ok).length;
|
const withIssues = j.results.filter(r => !r.ok).length;
|
||||||
@@ -228,12 +264,13 @@ func MraidPickFiles(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MraidScan(w http.ResponseWriter, r *http.Request) {
|
func MraidScan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
send := newJSONStream(w)
|
||||||
var req struct {
|
var req struct {
|
||||||
Folder string `json:"folder"`
|
Folder string `json:"folder"`
|
||||||
Files []string `json:"files"`
|
Files []string `json:"files"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeJSON(w, map[string]any{"results": []mraidResult{}, "error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error(), "results": []mraidResult{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,16 +305,17 @@ func MraidScan(w http.ResponseWriter, r *http.Request) {
|
|||||||
noTargetsError = "No AppLovin, ironSource, or Unity folder HTML files were found."
|
noTargetsError = "No AppLovin, ironSource, or Unity folder HTML files were found."
|
||||||
baseDir = req.Folder
|
baseDir = req.Folder
|
||||||
} else {
|
} else {
|
||||||
writeJSON(w, map[string]any{"results": []mraidResult{}, "error": "Pick a folder or files first"})
|
send(map[string]any{"type": "error", "message": "Pick a folder or files first", "results": []mraidResult{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
writeJSON(w, map[string]any{"results": []mraidResult{}, "skipped": skipped, "skippedReason": skippedReason, "error": noTargetsError})
|
send(map[string]any{"type": "error", "message": noTargetsError, "results": []mraidResult{}, "skipped": skipped, "skippedReason": skippedReason})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results := []mraidResult{}
|
results := []mraidResult{}
|
||||||
for _, file := range targets {
|
for i, file := range targets {
|
||||||
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Scanning %d/%d %s", i+1, len(targets), filepath.Base(file))})
|
||||||
data, err := os.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -293,7 +331,7 @@ func MraidScan(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
results = append(results, mraidResult{File: display, OK: len(issues) == 0, Issues: issues})
|
results = append(results, mraidResult{File: display, OK: len(issues) == 0, Issues: issues})
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{"results": results, "skipped": skipped, "skippedReason": skippedReason})
|
send(map[string]any{"type": "done", "results": results, "skipped": skipped, "skippedReason": skippedReason})
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -114,8 +114,7 @@ func SaveConfig(cfg AppConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func OpenInBrowser(url string) {
|
func OpenInBrowser(url string) {
|
||||||
// `start` is a cmd builtin. Quoted empty arg is the window title.
|
cmd := hiddenCommand("rundll32.exe", "url.dll,FileProtocolHandler", url)
|
||||||
cmd := hiddenCommand("cmd", "/c", "start", "", url)
|
|
||||||
_ = cmd.Start()
|
_ = cmd.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,25 @@ const statusEl = document.getElementById('status');
|
|||||||
const resultsEl = document.getElementById('results');
|
const resultsEl = document.getElementById('results');
|
||||||
const sourceSelectionEl = document.getElementById('sourceSelection');
|
const sourceSelectionEl = document.getElementById('sourceSelection');
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
function updateConvertBtn() {
|
function updateConvertBtn() {
|
||||||
const hasNet = [...document.querySelectorAll('.net-cb')].some(c => c.checked);
|
const hasNet = [...document.querySelectorAll('.net-cb')].some(c => c.checked);
|
||||||
convertBtn.disabled = !srcEl.value || !outEl.value || !baseNameEl.value.trim() || !hasNet;
|
convertBtn.disabled = !srcEl.value || !outEl.value || !baseNameEl.value.trim() || !hasNet;
|
||||||
@@ -215,9 +234,24 @@ document.getElementById('convert').addEventListener('click', async () => {
|
|||||||
headers:{'Content-Type':'application/json'},
|
headers:{'Content-Type':'application/json'},
|
||||||
body: JSON.stringify({ sourcePath: srcEl.value, outputDir: outEl.value, baseName: baseNameEl.value, networks })
|
body: JSON.stringify({ sourcePath: srcEl.value, outputDir: outEl.value, baseName: baseNameEl.value, networks })
|
||||||
});
|
});
|
||||||
const msg = await res.json();
|
let msg = null;
|
||||||
|
try {
|
||||||
|
await readJSONStream(res, (event) => {
|
||||||
|
if (event.type === 'status') {
|
||||||
|
statusEl.textContent = event.message;
|
||||||
|
statusEl.classList.add('is-busy');
|
||||||
|
} else if (event.type === 'done') {
|
||||||
|
msg = event;
|
||||||
|
} else if (event.type === 'error') {
|
||||||
|
msg = { error: event.message, results: event.results || [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
msg = { error: e.message || e, results: [] };
|
||||||
|
}
|
||||||
convertBtn.disabled = false;
|
convertBtn.disabled = false;
|
||||||
updateConvertBtn();
|
updateConvertBtn();
|
||||||
|
if (!msg) { msg = { error: 'No response.', results: [] }; }
|
||||||
if (msg.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + msg.error; return; }
|
if (msg.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + msg.error; return; }
|
||||||
const results = msg.results || [];
|
const results = msg.results || [];
|
||||||
const ok = results.filter(r => r.ok).length;
|
const ok = results.filter(r => r.ok).length;
|
||||||
@@ -256,33 +290,37 @@ func PlayworksPickOutput(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
|
func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
|
||||||
|
send := newJSONStream(w)
|
||||||
var opts playworksConvertOptions
|
var opts playworksConvertOptions
|
||||||
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||||
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error(), "results": []playworksNetworkResult{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !fileExists(opts.SourcePath) {
|
if !fileExists(opts.SourcePath) {
|
||||||
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Source file not found."})
|
send(map[string]any{"type": "error", "message": "Source file not found.", "results": []playworksNetworkResult{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
|
if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
|
||||||
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not create output folder."})
|
send(map[string]any{"type": "error", "message": "Could not create output folder.", "results": []playworksNetworkResult{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
send(map[string]any{"type": "status", "message": "Reading source HTML..."})
|
||||||
data, err := os.ReadFile(opts.SourcePath)
|
data, err := os.ReadFile(opts.SourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not read source: " + err.Error()})
|
send(map[string]any{"type": "error", "message": "Could not read source: " + err.Error(), "results": []playworksNetworkResult{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results := []playworksNetworkResult{}
|
results := []playworksNetworkResult{}
|
||||||
|
send(map[string]any{"type": "status", "message": "Converting Unity..."})
|
||||||
unityPath, err := copyPlayworksUnityInput(data, opts)
|
unityPath, err := copyPlayworksUnityInput(data, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results = append(results, playworksNetworkResult{Network: "un", OK: false, Error: err.Error()})
|
results = append(results, playworksNetworkResult{Network: "un", OK: false, Error: err.Error()})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, playworksNetworkResult{Network: "un", File: unityPath, OK: true})
|
results = append(results, playworksNetworkResult{Network: "un", File: unityPath, OK: true})
|
||||||
}
|
}
|
||||||
for _, network := range opts.Networks {
|
for i, network := range opts.Networks {
|
||||||
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Converting %d/%d %s", i+1, len(opts.Networks), network)})
|
||||||
filePath, err := convertPlayworksNetwork(string(data), opts, network)
|
filePath, err := convertPlayworksNetwork(string(data), opts, network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results = append(results, playworksNetworkResult{Network: network, OK: false, Error: err.Error()})
|
results = append(results, playworksNetworkResult{Network: network, OK: false, Error: err.Error()})
|
||||||
@@ -290,7 +328,7 @@ func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
results = append(results, playworksNetworkResult{Network: network, File: filePath, OK: true})
|
results = append(results, playworksNetworkResult{Network: network, File: filePath, OK: true})
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{"results": results})
|
send(map[string]any{"type": "done", "results": results})
|
||||||
}
|
}
|
||||||
|
|
||||||
var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|ap|mo|vu|mtg|tt)$`)
|
var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|ap|mo|vu|mtg|tt)$`)
|
||||||
|
|||||||
@@ -100,6 +100,24 @@ function addFiles(files) {
|
|||||||
page = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
|
page = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
|
||||||
renderRows();
|
renderRows();
|
||||||
}
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
function renderRows() {
|
function renderRows() {
|
||||||
const oldPager = document.getElementById('selectedPager');
|
const oldPager = document.getElementById('selectedPager');
|
||||||
if (oldPager) oldPager.remove();
|
if (oldPager) oldPager.remove();
|
||||||
@@ -183,12 +201,31 @@ document.getElementById('upload').addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
statusEl.textContent = 'Uploading...';
|
statusEl.textContent = 'Uploading...';
|
||||||
statusEl.classList.add('is-busy');
|
statusEl.classList.add('is-busy');
|
||||||
const res = await fetch('/api/plec/upload', {
|
let j = null;
|
||||||
method: 'POST',
|
try {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const res = await fetch('/api/plec/upload', {
|
||||||
body: JSON.stringify({ rows }),
|
method: 'POST',
|
||||||
});
|
headers: { 'Content-Type': 'application/json' },
|
||||||
const j = await res.json();
|
body: JSON.stringify({ rows }),
|
||||||
|
});
|
||||||
|
await readJSONStream(res, (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 };
|
||||||
|
} else if (msg.type === 'rowErrors') {
|
||||||
|
j = { rowErrors: msg.rowErrors };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
statusEl.classList.remove('is-busy');
|
||||||
|
statusEl.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!j) { statusEl.classList.remove('is-busy'); statusEl.innerHTML = '<span class="err">No response.</span>'; return; }
|
||||||
if (j.rowErrors) {
|
if (j.rowErrors) {
|
||||||
statusEl.classList.remove('is-busy');
|
statusEl.classList.remove('is-busy');
|
||||||
for (const re of j.rowErrors) {
|
for (const re of j.rowErrors) {
|
||||||
@@ -260,11 +297,12 @@ type plecRow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
send := newJSONStream(w)
|
||||||
var req struct {
|
var req struct {
|
||||||
Rows []plecRow `json:"rows"`
|
Rows []plecRow `json:"rows"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
cfg := LoadConfig().Plec
|
cfg := LoadConfig().Plec
|
||||||
@@ -289,11 +327,11 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(rowErrors) > 0 {
|
if len(rowErrors) > 0 {
|
||||||
writeJSON(w, map[string]any{"rowErrors": rowErrors})
|
send(map[string]any{"type": "rowErrors", "rowErrors": rowErrors})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(valid) == 0 {
|
if len(valid) == 0 {
|
||||||
writeJSON(w, map[string]any{"error": "Nothing to upload."})
|
send(map[string]any{"type": "error", "message": "Nothing to upload."})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,37 +341,39 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
mw := multipart.NewWriter(&buf)
|
mw := multipart.NewWriter(&buf)
|
||||||
for i, row := range valid {
|
for i, row := range valid {
|
||||||
idx := i + 1
|
idx := i + 1
|
||||||
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Preparing %d/%d %s", idx, len(valid), filepath.Base(row.Path))})
|
||||||
data, err := os.ReadFile(row.Path)
|
data, err := os.ReadFile(row.Path)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
stampedName := appendTimestamp(filepath.Base(row.Path), stamp)
|
stampedName := appendTimestamp(filepath.Base(row.Path), stamp)
|
||||||
fw, err := createFormFileWithType(mw, fmt.Sprintf("fileUpload_%d", idx), stampedName, "text/html")
|
fw, err := createFormFileWithType(mw, fmt.Sprintf("fileUpload_%d", idx), stampedName, "text/html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, map[string]any{"error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := fw.Write(data); err != nil {
|
if _, err := fw.Write(data); err != nil {
|
||||||
writeJSON(w, map[string]any{"error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
iter := urlpkg.QueryEscape(strings.ReplaceAll(row.Iteration, " ", ""))
|
iter := urlpkg.QueryEscape(strings.ReplaceAll(row.Iteration, " ", ""))
|
||||||
// Original uses encodeURI which keeps more chars than QueryEscape; emulate by un-escaping safe chars.
|
// Original uses encodeURI which keeps more chars than QueryEscape; emulate by un-escaping safe chars.
|
||||||
iter = encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", ""))
|
iter = encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", ""))
|
||||||
if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil {
|
if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil {
|
||||||
writeJSON(w, map[string]any{"error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := mw.Close(); err != nil {
|
if err := mw.Close(); err != nil {
|
||||||
writeJSON(w, map[string]any{"error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d file%s...", len(valid), pluralS(len(valid)))})
|
||||||
httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf)
|
httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, map[string]any{"error": err.Error()})
|
send(map[string]any{"type": "error", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", mw.FormDataContentType())
|
httpReq.Header.Set("Content-Type", mw.FormDataContentType())
|
||||||
@@ -345,18 +385,18 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
resp, err := http.DefaultClient.Do(httpReq)
|
resp, err := http.DefaultClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, map[string]any{"error": "Network error: " + err.Error()})
|
send(map[string]any{"type": "error", "message": "Network error: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
writeJSON(w, map[string]any{"error": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))})
|
send(map[string]any{"type": "error", "message": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var parsed map[string]any
|
var parsed map[string]any
|
||||||
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||||
writeJSON(w, map[string]any{"error": "Server returned non-JSON:\n" + truncate(string(respBody), 800)})
|
send(map[string]any{"type": "error", "message": "Server returned non-JSON:\n" + truncate(string(respBody), 800)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var preview any
|
var preview any
|
||||||
@@ -365,7 +405,7 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else if v, ok := parsed["previewURL"]; ok {
|
} else if v, ok := parsed["previewURL"]; ok {
|
||||||
preview = v
|
preview = v
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{"preview": preview, "raw": parsed, "rawText": string(respBody)})
|
send(map[string]any{"type": "done", "preview": preview, "raw": parsed, "rawText": string(respBody)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClipboardEndpoint(w http.ResponseWriter, r *http.Request) {
|
func ClipboardEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user