fix: doodstream upload, start selected, UI improvements
- Fix DoodStream upload: parse <textarea> fields (not just <input hidden>) - Fix DoodStream upload: handle redirect responses from upload server - Fix DoodStream upload: submit upload_result to doodstream.com (not CDN) - Fix DoodStream speed display: switch to async generator streaming - Add "Start Selected" toolbar button to upload only selected queue items - Move "Always on Top" from context menu to Settings - Remove "Shutdown after Finish" from context menu - Hide error entries from upload history (only show successful uploads) - Disable background throttling to prevent UI lag on focus switch - Add debug logging for DoodStream upload troubleshooting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8995feb541
commit
e38c55988c
@ -7,6 +7,13 @@ const BASE_URL = 'https://doodstream.com';
|
||||
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||
const UPLOAD_TIMEOUT = 1800000; // 30 min
|
||||
|
||||
function _debugLog(msg) {
|
||||
try {
|
||||
const ts = new Date().toISOString();
|
||||
fs.appendFileSync(path.join(process.cwd(), 'doodstream-debug.log'), `[${ts}] ${msg}\n`);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
class DoodstreamUploader {
|
||||
constructor() {
|
||||
this.cookies = new Map();
|
||||
@ -193,73 +200,34 @@ class DoodstreamUploader {
|
||||
|
||||
// Build multipart form
|
||||
const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`;
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
|
||||
// Build form parts
|
||||
const fields = {
|
||||
sess_id: this.sessId,
|
||||
utype: 'reg'
|
||||
};
|
||||
let preamble = '';
|
||||
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="sess_id"\r\n\r\n${this.sessId}\r\n`;
|
||||
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="utype"\r\n\r\nreg\r\n`;
|
||||
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`;
|
||||
|
||||
const preamble = [];
|
||||
for (const [key, val] of Object.entries(fields)) {
|
||||
preamble.push(
|
||||
`--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${val}\r\n`
|
||||
);
|
||||
const epilogue = `\r\n--${boundary}--\r\n`;
|
||||
const preambleBuf = Buffer.from(preamble, 'utf-8');
|
||||
const epilogueBuf = Buffer.from(epilogue, 'utf-8');
|
||||
const totalSize = preambleBuf.length + fileSize + epilogueBuf.length;
|
||||
|
||||
const CHUNK_SIZE = 256 * 1024;
|
||||
let bytesRead = 0;
|
||||
|
||||
const self = this;
|
||||
async function* generate() {
|
||||
yield preambleBuf;
|
||||
const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE });
|
||||
for await (const chunk of fileStream) {
|
||||
if (signal && signal.aborted) throw new Error('Aborted');
|
||||
if (throttle) await throttle.consume(chunk.length, signal);
|
||||
bytesRead += chunk.length;
|
||||
yield chunk;
|
||||
if (progressCb) progressCb(bytesRead, fileSize);
|
||||
}
|
||||
yield epilogueBuf;
|
||||
}
|
||||
preamble.push(
|
||||
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`
|
||||
);
|
||||
|
||||
const preambleBuffer = Buffer.from(preamble.join(''));
|
||||
const epilogue = Buffer.from(`\r\n--${boundary}--\r\n`);
|
||||
const totalSize = preambleBuffer.length + fileSize + epilogue.length;
|
||||
|
||||
// Assemble body
|
||||
const { Readable } = require('stream');
|
||||
let bytesSent = 0;
|
||||
|
||||
const bodyStream = new Readable({
|
||||
read() {}
|
||||
});
|
||||
|
||||
// Push preamble
|
||||
bodyStream.push(preambleBuffer);
|
||||
bytesSent += preambleBuffer.length;
|
||||
|
||||
// Pipe file with throttle support
|
||||
fileStream.on('data', (chunk) => {
|
||||
if (signal && signal.aborted) {
|
||||
fileStream.destroy();
|
||||
bodyStream.destroy();
|
||||
return;
|
||||
}
|
||||
if (throttle) {
|
||||
fileStream.pause();
|
||||
throttle.consume(chunk.length, signal).then(() => {
|
||||
bodyStream.push(chunk);
|
||||
bytesSent += chunk.length;
|
||||
if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize);
|
||||
fileStream.resume();
|
||||
}).catch(() => {
|
||||
fileStream.destroy();
|
||||
bodyStream.destroy();
|
||||
});
|
||||
} else {
|
||||
bodyStream.push(chunk);
|
||||
bytesSent += chunk.length;
|
||||
if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize);
|
||||
}
|
||||
});
|
||||
|
||||
fileStream.on('end', () => {
|
||||
bodyStream.push(epilogue);
|
||||
bodyStream.push(null);
|
||||
});
|
||||
|
||||
fileStream.on('error', (err) => {
|
||||
bodyStream.destroy(err);
|
||||
});
|
||||
|
||||
const uploadRes = await request(uploadUrl, {
|
||||
method: 'POST',
|
||||
@ -269,83 +237,190 @@ class DoodstreamUploader {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Cookie': this._cookieHeader()
|
||||
},
|
||||
body: bodyStream,
|
||||
body: generate(),
|
||||
signal,
|
||||
bodyTimeout: UPLOAD_TIMEOUT,
|
||||
headersTimeout: 60000
|
||||
});
|
||||
|
||||
const statusCode = uploadRes.statusCode;
|
||||
_debugLog(`Upload response status: ${statusCode}`);
|
||||
|
||||
// Handle redirects from upload server (undici doesn't follow them)
|
||||
if ([301, 302, 303, 307, 308].includes(statusCode)) {
|
||||
const location = uploadRes.headers['location'];
|
||||
try { await uploadRes.body.text(); } catch {}
|
||||
_debugLog(`Upload redirect to: ${location}`);
|
||||
if (location) {
|
||||
return this._handleUploadResult(location);
|
||||
}
|
||||
}
|
||||
|
||||
const resText = await uploadRes.body.text();
|
||||
let payload;
|
||||
try { payload = JSON.parse(resText); } catch {}
|
||||
_debugLog(`Upload response body (first 500): ${resText.slice(0, 500)}`);
|
||||
|
||||
if (statusCode >= 400) {
|
||||
let payload;
|
||||
try { payload = JSON.parse(resText); } catch {}
|
||||
const msg = payload && payload.msg ? payload.msg : resText.slice(0, 200);
|
||||
throw new Error(`Doodstream Upload HTTP ${statusCode}: ${msg}`);
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
// Try to extract filecode directly from HTML
|
||||
const codeMatch = resText.match(/filecode['":\s]+['"]([a-zA-Z0-9]+)['"]/i);
|
||||
if (codeMatch) {
|
||||
return {
|
||||
download_url: `https://doodstream.com/d/${codeMatch[1]}`,
|
||||
embed_url: `https://doodstream.com/e/${codeMatch[1]}`,
|
||||
file_code: codeMatch[1]
|
||||
};
|
||||
}
|
||||
return this._parseUploadResponse(resText);
|
||||
}
|
||||
|
||||
// Follow HTML form redirect (two-step upload)
|
||||
const formAction = resText.match(/<form[^>]*action=['"]([^'"]+)['"]/i);
|
||||
if (formAction) {
|
||||
const hiddenFields = {};
|
||||
const inputRegex = /<input[^>]*type=['"]hidden['"][^>]*name=['"]([^'"]+)['"][^>]*value=['"]([^'"]*)['"]/gi;
|
||||
let m;
|
||||
while ((m = inputRegex.exec(resText)) !== null) {
|
||||
hiddenFields[m[1]] = m[2];
|
||||
}
|
||||
// Also try reversed attribute order (value before name)
|
||||
const inputRegex2 = /<input[^>]*value=['"]([^'"]*)['""][^>]*name=['"]([^'"]+)['"]/gi;
|
||||
while ((m = inputRegex2.exec(resText)) !== null) {
|
||||
if (!hiddenFields[m[2]]) hiddenFields[m[2]] = m[1];
|
||||
}
|
||||
/**
|
||||
* Follow a redirect URL from upload server and extract filecode
|
||||
*/
|
||||
async _handleUploadResult(url) {
|
||||
_debugLog(`Following upload result URL: ${url}`);
|
||||
const res = await this._fetch(url);
|
||||
const html = await res.text();
|
||||
_debugLog(`Result page (first 500): ${html.slice(0, 500)}`);
|
||||
return this._parseUploadResponse(html);
|
||||
}
|
||||
|
||||
const formData = new URLSearchParams(hiddenFields);
|
||||
const followRes = await this._fetch(formAction[1], {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString()
|
||||
});
|
||||
const followText = await followRes.text();
|
||||
/**
|
||||
* Extract hidden form fields from HTML (handles various attribute orders)
|
||||
*/
|
||||
_extractHiddenFields(html) {
|
||||
const fields = {};
|
||||
// Textarea fields: <textarea name="op">upload_result</textarea>
|
||||
const ta = /<textarea[^>]*name=['"]([^'"]+)['"][^>]*>([\s\S]*?)<\/textarea>/gi;
|
||||
let m;
|
||||
while ((m = ta.exec(html)) !== null) fields[m[1]] = m[2].trim();
|
||||
// Input hidden fields
|
||||
const p1 = /<input[^>]*type=['"]hidden['"][^>]*name=['"]([^'"]+)['"][^>]*value=['"]([^'"]*)['"]/gi;
|
||||
while ((m = p1.exec(html)) !== null) { if (!fields[m[1]]) fields[m[1]] = m[2]; }
|
||||
const p2 = /<input[^>]*name=['"]([^'"]+)['"][^>]*value=['"]([^'"]*)['"]/gi;
|
||||
while ((m = p2.exec(html)) !== null) { if (!fields[m[1]]) fields[m[1]] = m[2]; }
|
||||
const p3 = /<input[^>]*value=['"]([^'"]*)['"]\s[^>]*name=['"]([^'"]+)['"]/gi;
|
||||
while ((m = p3.exec(html)) !== null) { if (!fields[m[2]]) fields[m[2]] = m[1]; }
|
||||
return fields;
|
||||
}
|
||||
|
||||
// Try JSON from follow response
|
||||
let followPayload;
|
||||
try { followPayload = JSON.parse(followText); } catch {}
|
||||
if (followPayload) {
|
||||
payload = followPayload;
|
||||
} else {
|
||||
// Try filecode from follow response HTML
|
||||
const followCode = followText.match(/filecode['":\s]+['"]([a-zA-Z0-9]+)['"]/i);
|
||||
if (followCode) {
|
||||
return {
|
||||
download_url: `https://doodstream.com/d/${followCode[1]}`,
|
||||
embed_url: `https://doodstream.com/e/${followCode[1]}`,
|
||||
file_code: followCode[1]
|
||||
};
|
||||
}
|
||||
throw new Error(`Doodstream Upload: Redirect-Antwort ungueltig (${followText.slice(0, 150)})`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Doodstream Upload: Keine gueltige Antwort (HTTP ${statusCode}, Body: ${resText.slice(0, 150)})`);
|
||||
}
|
||||
/**
|
||||
* Parse filecode from upload server response (JSON or HTML)
|
||||
*/
|
||||
async _parseUploadResponse(resText) {
|
||||
// 1. Try JSON
|
||||
let payload;
|
||||
try { payload = JSON.parse(resText); } catch {}
|
||||
|
||||
if (payload) {
|
||||
return this._extractFromJson(payload);
|
||||
}
|
||||
|
||||
// 2. Try filecode directly in HTML
|
||||
const code = this._findFilecodeInHtml(resText);
|
||||
if (code) {
|
||||
_debugLog(`Found filecode in HTML: ${code}`);
|
||||
return this._buildResult(code);
|
||||
}
|
||||
|
||||
// 3. Parse HTML form (XFileSharing two-step upload)
|
||||
const hiddenFields = this._extractHiddenFields(resText);
|
||||
_debugLog(`Hidden fields: ${JSON.stringify(hiddenFields)}`);
|
||||
|
||||
// Check if filecode is already in hidden fields
|
||||
const fnCode = hiddenFields.fn || hiddenFields.filecode || hiddenFields.file_code;
|
||||
if (fnCode && fnCode.length >= 8) {
|
||||
_debugLog(`Filecode from hidden field 'fn': ${fnCode}`);
|
||||
// We still need to submit the form so doodstream registers the file
|
||||
// But the filecode is the 'fn' value
|
||||
}
|
||||
|
||||
// XFileSharing standard: form with op=upload_result, fn, st
|
||||
// Always submit to doodstream.com, not to CDN
|
||||
if (hiddenFields.fn || hiddenFields.op === 'upload_result') {
|
||||
// Ensure op=upload_result is set
|
||||
if (!hiddenFields.op) hiddenFields.op = 'upload_result';
|
||||
|
||||
_debugLog(`Submitting upload_result to ${BASE_URL}/ with fields: ${JSON.stringify(hiddenFields)}`);
|
||||
const formData = new URLSearchParams(hiddenFields);
|
||||
const followRes = await this._fetch(BASE_URL + '/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Referer': BASE_URL + '/'
|
||||
},
|
||||
body: formData.toString()
|
||||
});
|
||||
const followText = await followRes.text();
|
||||
_debugLog(`upload_result response (first 500): ${followText.slice(0, 500)}`);
|
||||
|
||||
// Try to find filecode in result page
|
||||
const resultCode = this._findFilecodeInHtml(followText);
|
||||
if (resultCode) {
|
||||
return this._buildResult(resultCode);
|
||||
}
|
||||
|
||||
// If we had fn from hidden fields, use that as filecode
|
||||
if (fnCode && fnCode.length >= 8) {
|
||||
return this._buildResult(fnCode);
|
||||
}
|
||||
|
||||
// Try download URL pattern in result page
|
||||
const dlMatch = followText.match(/https?:\/\/[a-z0-9.]+\/d\/([a-zA-Z0-9]+)/i);
|
||||
if (dlMatch) {
|
||||
return this._buildResult(dlMatch[1]);
|
||||
}
|
||||
|
||||
throw new Error(`Doodstream Upload: upload_result Seite hat keinen filecode (${followText.slice(0, 150)})`);
|
||||
}
|
||||
|
||||
// 4. Fallback: follow form action as-is (for non-XFS forms)
|
||||
const formAction = resText.match(/<form[^>]*action=['"]([^'"]+)['"]/i);
|
||||
if (formAction) {
|
||||
_debugLog(`Fallback: following form action ${formAction[1]}`);
|
||||
const formData = new URLSearchParams(hiddenFields);
|
||||
const followRes = await this._fetch(formAction[1], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Referer': BASE_URL + '/'
|
||||
},
|
||||
body: formData.toString()
|
||||
});
|
||||
const followText = await followRes.text();
|
||||
_debugLog(`Fallback response (first 500): ${followText.slice(0, 500)}`);
|
||||
|
||||
const fallbackCode = this._findFilecodeInHtml(followText);
|
||||
if (fallbackCode) return this._buildResult(fallbackCode);
|
||||
|
||||
// Check if fn was in original hidden fields
|
||||
if (fnCode && fnCode.length >= 8) return this._buildResult(fnCode);
|
||||
|
||||
throw new Error(`Doodstream Upload: Redirect-Antwort ungueltig (${followText.slice(0, 150)})`);
|
||||
}
|
||||
|
||||
throw new Error(`Doodstream Upload: Keine gueltige Antwort (Body: ${resText.slice(0, 150)})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for filecode patterns in HTML
|
||||
*/
|
||||
_findFilecodeInHtml(html) {
|
||||
// filecode: "xxx" or filecode = "xxx"
|
||||
const m1 = html.match(/filecode['":\s]+['"]([a-zA-Z0-9]{8,})['"]/i);
|
||||
if (m1) return m1[1];
|
||||
// file_code: "xxx"
|
||||
const m2 = html.match(/file_code['":\s]+['"]([a-zA-Z0-9]{8,})['"]/i);
|
||||
if (m2) return m2[1];
|
||||
// Download URL pattern: /d/FILECODE
|
||||
const m3 = html.match(/\/d\/([a-zA-Z0-9]{8,})/);
|
||||
if (m3) return m3[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract result from JSON payload
|
||||
*/
|
||||
_extractFromJson(payload) {
|
||||
if (payload.status && Number(payload.status) !== 200 && payload.msg) {
|
||||
throw new Error(`Doodstream Upload: ${payload.msg}`);
|
||||
}
|
||||
|
||||
// Parse result
|
||||
let item = null;
|
||||
const result = payload.result;
|
||||
if (Array.isArray(result) && result.length > 0) {
|
||||
@ -359,13 +434,20 @@ class DoodstreamUploader {
|
||||
}
|
||||
|
||||
const fileCode = item.filecode || item.file_code || '';
|
||||
|
||||
return {
|
||||
download_url: item.download_url || item.protected_dl || (fileCode ? `https://doodstream.com/d/${fileCode}` : null),
|
||||
embed_url: item.protected_embed || (fileCode ? `https://doodstream.com/e/${fileCode}` : null),
|
||||
file_code: fileCode
|
||||
};
|
||||
}
|
||||
|
||||
_buildResult(fileCode) {
|
||||
return {
|
||||
download_url: `https://doodstream.com/d/${fileCode}`,
|
||||
embed_url: `https://doodstream.com/e/${fileCode}`,
|
||||
file_code: fileCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DoodstreamUploader;
|
||||
|
||||
1
main.js
1
main.js
@ -425,6 +425,7 @@ function createWindow() {
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.webContents.setBackgroundThrottling(false);
|
||||
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||
}
|
||||
|
||||
|
||||
@ -476,8 +476,10 @@ function updateQueueActionButtons() {
|
||||
const hasSelection = selectedJobIds.size > 0;
|
||||
const hasUploadSelection = queueJobs.some((job) => selectedJobIds.has(job.id) && ['done', 'error', 'aborted', 'skipped'].includes(job.status));
|
||||
const hasAbortSelection = queueJobs.some((job) => selectedJobIds.has(job.id) && ['preview', 'queued', 'getting-server', 'uploading', 'retrying'].includes(job.status));
|
||||
const hasStartableSelection = queueJobs.some((job) => selectedJobIds.has(job.id) && ['preview', 'queued'].includes(job.status));
|
||||
const hasMovableSelection = hasSelection && !uploading;
|
||||
|
||||
const startSelectedBtn = document.getElementById('startSelectedBtn');
|
||||
const reuploadBtn = document.getElementById('reuploadSelectedBtn');
|
||||
const abortSelectedBtn = document.getElementById('abortSelectedBtn');
|
||||
const finishStopBtn = document.getElementById('finishStopBtn');
|
||||
@ -487,6 +489,7 @@ function updateQueueActionButtons() {
|
||||
const moveDownBtn = document.getElementById('moveDownBtn');
|
||||
const moveBottomBtn = document.getElementById('moveBottomBtn');
|
||||
|
||||
if (startSelectedBtn) startSelectedBtn.disabled = uploading || !hasStartableSelection || getSelectedHosters().length === 0;
|
||||
if (reuploadBtn) reuploadBtn.disabled = !hasUploadSelection;
|
||||
if (abortSelectedBtn) abortSelectedBtn.disabled = !hasAbortSelection;
|
||||
if (finishStopBtn) finishStopBtn.disabled = !uploading;
|
||||
@ -921,6 +924,64 @@ async function startUpload() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startSelectedUpload() {
|
||||
if (healthCheckRunning || uploading) return;
|
||||
|
||||
const hosters = getSelectedHosters();
|
||||
if (hosters.length === 0) { alert('Bitte mindestens einen Hoster auswählen.'); return; }
|
||||
|
||||
const jobsToStart = queueJobs.filter((job) => selectedJobIds.has(job.id) && (job.status === 'preview' || job.status === 'queued'));
|
||||
if (jobsToStart.length === 0) return;
|
||||
|
||||
// Auto health check
|
||||
if (autoHealthCheckEnabled) {
|
||||
const checkHosters = hosters.filter(name => name === 'doodstream.com' || name === 'vidmoly.me' || name === 'voe.sx' || name === 'byse.sx');
|
||||
if (checkHosters.length > 0) {
|
||||
healthCheckRunning = true;
|
||||
try {
|
||||
const rows = await executeHealthCheck(checkHosters, 'auto');
|
||||
const errors = rows.filter(r => r.status === 'error');
|
||||
if (errors.length > 0) {
|
||||
alert(`Auto-Check fehlgeschlagen:\n${errors.map(r => `${r.hoster}: ${r.message}`).join('\n')}\n\nUpload wurde nicht gestartet.`);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Auto-Check fehlgeschlagen: ${err.message}\nUpload wurde nicht gestartet.`);
|
||||
return;
|
||||
} finally {
|
||||
healthCheckRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploading = true;
|
||||
jobsToStart.forEach(j => {
|
||||
if (j.status === 'preview') j.status = 'queued';
|
||||
});
|
||||
updateQueueActionButtons();
|
||||
renderQueueTable();
|
||||
updateStatusBar();
|
||||
|
||||
const uploadPayload = {
|
||||
hosters,
|
||||
jobs: jobsToStart.map((job) => ({
|
||||
id: job.id,
|
||||
file: job.file,
|
||||
fileName: job.fileName,
|
||||
hoster: job.hoster
|
||||
}))
|
||||
};
|
||||
const result = await window.api.startUpload(uploadPayload);
|
||||
persistQueueStateSoon();
|
||||
|
||||
if (result && result.error) {
|
||||
alert(result.error);
|
||||
uploading = false;
|
||||
updateQueueActionButtons();
|
||||
updateStatusBar();
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelUpload() {
|
||||
await window.api.cancelUpload();
|
||||
uploading = false;
|
||||
@ -1373,6 +1434,10 @@ function renderSettings() {
|
||||
</div>
|
||||
<div class="settings-section-label">Verhalten</div>
|
||||
<div class="settings-grid-mini">
|
||||
<div class="settings-row checkbox-row">
|
||||
<label>Immer im Vordergrund</label>
|
||||
<input type="checkbox" class="settings-autosave" id="alwaysOnTopInput" ${alwaysOnTopState ? 'checked' : ''}>
|
||||
</div>
|
||||
<div class="settings-row checkbox-row">
|
||||
<label>Hoster-Limits hochskalieren</label>
|
||||
<input type="checkbox" class="settings-autosave" id="scaleParallelUploadsInput" ${globalSettings.scaleParallelUploads ? 'checked' : ''}>
|
||||
@ -1501,6 +1566,16 @@ async function saveSettings(options = {}) {
|
||||
globalMaxSpeedKbs: Math.max(0, Math.round((parseFloat(document.getElementById('globalMaxSpeedMbsInput')?.value || '0') || 0) * 1024))
|
||||
};
|
||||
|
||||
// Always on top setting
|
||||
const aotCheckbox = document.getElementById('alwaysOnTopInput');
|
||||
if (aotCheckbox) {
|
||||
const newAot = !!aotCheckbox.checked;
|
||||
if (newAot !== alwaysOnTopState) {
|
||||
alwaysOnTopState = newAot;
|
||||
await window.api.setAlwaysOnTop(alwaysOnTopState);
|
||||
}
|
||||
}
|
||||
|
||||
for (const name of HOSTERS) {
|
||||
const hs = { ...(hosterSettings[name] || {}) };
|
||||
document.querySelectorAll(`.hs-input[data-hoster="${name}"]`).forEach(input => {
|
||||
@ -1845,7 +1920,7 @@ async function loadHistory() {
|
||||
const dt = formatDateTime(batch.timestamp || new Date());
|
||||
for (const file of (batch.files || [])) {
|
||||
for (const result of (file.results || [])) {
|
||||
if (result.status === 'aborted') continue;
|
||||
if (result.status === 'aborted' || result.status === 'error') continue;
|
||||
const isError = result.status === 'error';
|
||||
historyRowsData.push({
|
||||
date: dt.text, dateTs: dt.ts,
|
||||
@ -1965,6 +2040,7 @@ function setupListeners() {
|
||||
document.getElementById('addFilesBtn').addEventListener('click', pickFiles);
|
||||
document.getElementById('chooseHostersBtn').addEventListener('click', openHosterModal);
|
||||
document.getElementById('startUploadBtn').addEventListener('click', startUpload);
|
||||
document.getElementById('startSelectedBtn').addEventListener('click', startSelectedUpload);
|
||||
document.getElementById('reuploadSelectedBtn').addEventListener('click', retrySelectedJobs);
|
||||
document.getElementById('abortSelectedBtn').addEventListener('click', abortSelectedJobs);
|
||||
document.getElementById('finishStopBtn').addEventListener('click', finishUploadsInProgress);
|
||||
|
||||
@ -40,9 +40,12 @@
|
||||
|
||||
<div class="queue-shell" id="queueShell" style="display:none">
|
||||
<div class="queue-command-bar" id="queueCommandBar">
|
||||
<button class="toolbar-btn" id="startUploadBtn" title="Start Uploading" disabled>
|
||||
<button class="toolbar-btn" id="startUploadBtn" title="Start all" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 2l10 6-10 6z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="startSelectedBtn" title="Start selected" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6 3l8 5-8 5z" fill="#4caf50"/><rect x="1" y="3" width="3" height="10" rx="0.5" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="reuploadSelectedBtn" title="Reupload selected file">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 1a7 7 0 0 0-5 2.1V1H2v4h4V4H3.7A5.5 5.5 0 1 1 2.5 8H1a7 7 0 1 0 7-7z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
@ -235,17 +238,6 @@
|
||||
<div class="ctx-item" data-action="delete-selected">Entfernen</div>
|
||||
<div class="ctx-separator"></div>
|
||||
<div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</div>
|
||||
<div class="ctx-separator"></div>
|
||||
<div class="ctx-item ctx-submenu" data-action="shutdown">
|
||||
Shutdown nach Finish
|
||||
<div class="ctx-submenu-items">
|
||||
<div class="ctx-item" data-action="shutdown-nothing">Nichts</div>
|
||||
<div class="ctx-item" data-action="shutdown-sleep">Ruhezustand</div>
|
||||
<div class="ctx-item" data-action="shutdown-shutdown">Herunterfahren</div>
|
||||
<div class="ctx-item" data-action="shutdown-restart">Neustart</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctx-item" data-action="always-on-top">Immer im Vordergrund</div>
|
||||
</div>
|
||||
|
||||
<div class="statusbar" id="statusbar">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user