release: Multi-Hoster-Upload 2.1.7
This commit is contained in:
+93
-31
@@ -425,8 +425,19 @@ async function init() {
|
||||
for (let i = 0; i < batch.length; i++) handleProgress(batch[i]);
|
||||
});
|
||||
}
|
||||
window.api.onUploadBatchDone((data) => {
|
||||
handleBatchDone(data);
|
||||
window.api.onUploadBatchDone(async (data) => {
|
||||
const summary = data && data.summary ? data.summary : data;
|
||||
handleBatchDone(summary);
|
||||
if (data && data.finalizationId && window.api.completeUploadFinalization) {
|
||||
if (_doneRemovalCoalescer) _doneRemovalCoalescer.drainSync();
|
||||
queuePersistThrottle.cancel();
|
||||
await window.api.completeUploadFinalization({
|
||||
finalizationId: data.finalizationId,
|
||||
pendingQueue: queueJobs.some((job) => !['done', 'skipped'].includes(job.status))
|
||||
? buildPersistedQueueState()
|
||||
: null
|
||||
});
|
||||
}
|
||||
});
|
||||
window.api.onUploadStats((data) => {
|
||||
handleStats(data);
|
||||
@@ -491,11 +502,16 @@ async function init() {
|
||||
// Inject new preview jobs into the running batch
|
||||
const newJobs = queueJobs.filter(j => j.status === 'preview' && newPaths.has(j.file));
|
||||
if (newJobs.length > 0) {
|
||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
||||
newJobs.forEach(j => { j.status = 'queued'; });
|
||||
renderQueueTable();
|
||||
window.api.addJobsToBatch({
|
||||
jobs: newJobs.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster }))
|
||||
}).then(result => { _markSkippedJobs(result); }).catch(() => {});
|
||||
jobs: newJobs.map(serializeUploadJob),
|
||||
sourceCleanupGroups: cleanupPreparation.groups
|
||||
}).then(result => {
|
||||
_markSkippedJobs(result);
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
}).catch(() => {});
|
||||
persistQueueStateSoon(true);
|
||||
}
|
||||
}
|
||||
@@ -574,7 +590,6 @@ function _isHistoryTabActive() {
|
||||
if (nextView) nextView.classList.add('active');
|
||||
activeTab = tab;
|
||||
syncTabIndicator(tab);
|
||||
syncUploadSpeedSparklineVisibility(tab.dataset.view);
|
||||
const activeSidebarButton = nextView?.querySelector('.view-sidebar-navigation > .view-sidebar-item.active, .settings-navigation > .settings-nav-button.active');
|
||||
_syncSidebarIndicator(activeSidebarButton, true);
|
||||
if (tab.dataset.view === 'history' && (_historyDirty || !_historyEverLoaded)) {
|
||||
@@ -1095,11 +1110,16 @@ function applyHosterSelection() {
|
||||
buildQueuePreview(); // creates 'preview' jobs for new files
|
||||
const newJobs = queueJobs.filter(j => j.status === 'preview' && pendingPaths.has(j.file));
|
||||
if (newJobs.length > 0) {
|
||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
||||
newJobs.forEach(j => { j.status = 'queued'; });
|
||||
renderQueueTable();
|
||||
window.api.addJobsToBatch({
|
||||
jobs: newJobs.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster }))
|
||||
}).then(result => { _markSkippedJobs(result); }).catch(() => {});
|
||||
jobs: newJobs.map(serializeUploadJob),
|
||||
sourceCleanupGroups: cleanupPreparation.groups
|
||||
}).then(result => {
|
||||
_markSkippedJobs(result);
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
}).catch(() => {});
|
||||
persistQueueStateSoon(true);
|
||||
}
|
||||
}
|
||||
@@ -1167,6 +1187,10 @@ function restoreQueueStateFromConfig() {
|
||||
remaining: 0,
|
||||
error: job.error || null,
|
||||
result: job.result || null,
|
||||
sourceCleanupToken: job.sourceCleanupToken || null,
|
||||
sourceCleanupRequiredHosters: Array.isArray(job.sourceCleanupRequiredHosters) ? [...job.sourceCleanupRequiredHosters] : [],
|
||||
sourceCleanupCompletedHosters: Array.isArray(job.sourceCleanupCompletedHosters) ? [...job.sourceCleanupCompletedHosters] : [],
|
||||
sourceCleanupFingerprint: job.sourceCleanupFingerprint || null,
|
||||
attempt: 0,
|
||||
maxAttempts: job.maxAttempts || 0,
|
||||
link: '',
|
||||
@@ -1239,6 +1263,10 @@ function buildPersistedQueueState() {
|
||||
bytesTotal: job.bytesTotal || 0,
|
||||
error: isTerminal ? (job.error || null) : null,
|
||||
result: isTerminal ? (job.result || null) : null,
|
||||
sourceCleanupToken: job.sourceCleanupToken || null,
|
||||
sourceCleanupRequiredHosters: Array.isArray(job.sourceCleanupRequiredHosters) ? [...job.sourceCleanupRequiredHosters] : [],
|
||||
sourceCleanupCompletedHosters: Array.isArray(job.sourceCleanupCompletedHosters) ? [...job.sourceCleanupCompletedHosters] : [],
|
||||
sourceCleanupFingerprint: job.sourceCleanupFingerprint || null,
|
||||
maxAttempts: job.maxAttempts || 0
|
||||
};
|
||||
})
|
||||
@@ -2691,6 +2719,24 @@ function getSelectedJobLinks() {
|
||||
}
|
||||
|
||||
// --- Upload ---
|
||||
function prepareSourceCleanup(jobs) {
|
||||
if (!config.globalSettings?.deleteSourceAfterSuccessfulUpload || !window.SourceCleanupPolicy) return { groups: [] };
|
||||
return window.SourceCleanupPolicy.prepareGroups(queueJobs, jobs, () => window.crypto.randomUUID(), 'win32');
|
||||
}
|
||||
|
||||
function serializeUploadJob(job) {
|
||||
return {
|
||||
id: job.id,
|
||||
file: job.file,
|
||||
fileName: job.fileName,
|
||||
hoster: job.hoster,
|
||||
sourceCleanupToken: job.sourceCleanupToken || null,
|
||||
sourceCleanupRequiredHosters: job.sourceCleanupRequiredHosters || [],
|
||||
sourceCleanupCompletedHosters: job.sourceCleanupCompletedHosters || [],
|
||||
sourceCleanupFingerprint: job.sourceCleanupFingerprint || null
|
||||
};
|
||||
}
|
||||
|
||||
async function startUpload(opts) {
|
||||
if (uploading) return;
|
||||
if (!(opts && opts._autoRetry)) _cancelAutoRetry(true);
|
||||
@@ -2714,6 +2760,7 @@ async function startUpload(opts) {
|
||||
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
||||
|
||||
try {
|
||||
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||
jobsToStart.forEach(j => {
|
||||
j.status = 'queued';
|
||||
j.error = null;
|
||||
@@ -2732,14 +2779,13 @@ async function startUpload(opts) {
|
||||
const uploadPayload = {
|
||||
hosters,
|
||||
isAutoRetry: !!(opts && opts._autoRetry),
|
||||
jobs: jobsToStart.map((job) => ({
|
||||
id: job.id,
|
||||
file: job.file,
|
||||
fileName: job.fileName,
|
||||
hoster: job.hoster
|
||||
}))
|
||||
jobs: jobsToStart.map(serializeUploadJob),
|
||||
sourceCleanupGroups: cleanupPreparation.groups
|
||||
};
|
||||
const result = await window.api.startUpload(uploadPayload);
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
}
|
||||
_markSkippedJobs(result);
|
||||
persistQueueStateSoon();
|
||||
|
||||
@@ -2779,6 +2825,7 @@ async function startSelectedUpload(explicitJobs) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
const cleanupPreparation = prepareSourceCleanup(addable);
|
||||
addable.forEach(j => {
|
||||
j.status = 'queued'; j.error = null; j.result = null;
|
||||
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
|
||||
@@ -2787,10 +2834,11 @@ async function startSelectedUpload(explicitJobs) {
|
||||
let result = null;
|
||||
try {
|
||||
result = await window.api.addJobsToBatch({
|
||||
jobs: addable.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster }))
|
||||
jobs: addable.map(serializeUploadJob),
|
||||
sourceCleanupGroups: cleanupPreparation.groups
|
||||
});
|
||||
} catch (err) {
|
||||
showCopyToast(`Jobs konnten nicht hinzugefuegt werden: ${err.message}`);
|
||||
showCopyToast(`Jobs konnten nicht hinzugefügt werden: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2803,6 +2851,9 @@ async function startSelectedUpload(explicitJobs) {
|
||||
return;
|
||||
}
|
||||
_markSkippedJobs(result);
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
}
|
||||
persistQueueStateSoon();
|
||||
const added = Number(result && result.added) || 0;
|
||||
// Use ASCII-only toast text here to avoid encoding artifacts on some systems.
|
||||
@@ -2815,7 +2866,7 @@ async function startSelectedUpload(explicitJobs) {
|
||||
if (alreadyInBatch > 0) toastParts.push(`${alreadyInBatch} bereits im Batch`);
|
||||
if (skipped > 0) toastParts.push(`${skipped} ohne gueltigen Account`);
|
||||
if (result && result.error) {
|
||||
showCopyToast(`Jobs konnten nicht hinzugefuegt werden: ${result.error}`);
|
||||
showCopyToast(`Jobs konnten nicht hinzugefügt werden: ${result.error}`);
|
||||
} else if (toastParts.length > 0) {
|
||||
showCopyToast(`Jobs: ${toastParts.join(', ')}`);
|
||||
} else {
|
||||
@@ -2832,6 +2883,7 @@ async function startSelectedUpload(explicitJobs) {
|
||||
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
||||
|
||||
try {
|
||||
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||
jobsToStart.forEach(j => {
|
||||
j.status = 'queued';
|
||||
j.error = null;
|
||||
@@ -2847,14 +2899,13 @@ async function startSelectedUpload(explicitJobs) {
|
||||
|
||||
const uploadPayload = {
|
||||
hosters,
|
||||
jobs: jobsToStart.map((job) => ({
|
||||
id: job.id,
|
||||
file: job.file,
|
||||
fileName: job.fileName,
|
||||
hoster: job.hoster
|
||||
}))
|
||||
jobs: jobsToStart.map(serializeUploadJob),
|
||||
sourceCleanupGroups: cleanupPreparation.groups
|
||||
};
|
||||
const result = await window.api.startUpload(uploadPayload);
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
}
|
||||
_markSkippedJobs(result);
|
||||
persistQueueStateSoon();
|
||||
|
||||
@@ -2972,6 +3023,7 @@ function _handleProgressImpl(data) {
|
||||
|
||||
// Track completed uploads so they don't get re-queued after removal
|
||||
if (job.status === 'done') {
|
||||
if (window.SourceCleanupPolicy) window.SourceCleanupPolicy.markCompleted(queueJobs, job, 'win32');
|
||||
_completedUploadKeys.add(`${job.file}|${job.hoster}`);
|
||||
}
|
||||
|
||||
@@ -3796,13 +3848,6 @@ function updateUploadSpeedDisplays() {
|
||||
_setUploadTelemetryText('uploadSpeedValue', text);
|
||||
}
|
||||
|
||||
function syncUploadSpeedSparklineVisibility(view) {
|
||||
const widget = document.getElementById('uploadSpeedSparkline');
|
||||
if (!widget) return;
|
||||
const activeView = view || document.querySelector('.tab.active')?.dataset.view;
|
||||
widget.classList.toggle('is-hidden', activeView !== 'upload');
|
||||
}
|
||||
|
||||
function drawUploadSpeedSparkline() {
|
||||
const canvas = document.getElementById('uploadSpeedCanvas');
|
||||
if (!canvas) return;
|
||||
@@ -3860,7 +3905,6 @@ function updateUploadSpeedSparkline() {
|
||||
|
||||
function initUploadSpeedSparkline() {
|
||||
if (uploadSpeedTimer !== null) return;
|
||||
syncUploadSpeedSparklineVisibility();
|
||||
updateUploadSpeedSparkline();
|
||||
uploadSpeedTimer = window.setInterval(updateUploadSpeedSparkline, 250);
|
||||
window.addEventListener('resize', drawUploadSpeedSparkline);
|
||||
@@ -4186,6 +4230,14 @@ function renderSettings() {
|
||||
</div>
|
||||
<input type="checkbox" class="settings-autosave" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}>
|
||||
</div>
|
||||
<div class="settings-section-label">Quelldateien</div>
|
||||
<div class="settings-option source-delete-option">
|
||||
<div class="settings-option-copy">
|
||||
<label for="deleteSourceAfterSuccessfulUploadInput">Quelldatei nach vollständigem Upload dauerhaft löschen</label>
|
||||
<span class="settings-option-description">Löscht die Originaldatei endgültig, sobald alle dafür ausgewählten Hoster erfolgreich abgeschlossen sind. Der Papierkorb wird nicht verwendet.</span>
|
||||
</div>
|
||||
<input type="checkbox" class="settings-autosave" id="deleteSourceAfterSuccessfulUploadInput" ${globalSettings.deleteSourceAfterSuccessfulUpload ? 'checked' : ''}>
|
||||
</div>
|
||||
<div class="settings-hoster-pointer"><strong>Einstellungen einzelner Hoster</strong> wie Wiederholungen, Geschwindigkeit, Parallelität, Dateigröße und Logging findest du im <strong>Accounts</strong>-Tab direkt beim jeweiligen Hoster.</div>
|
||||
`;
|
||||
|
||||
@@ -4715,11 +4767,20 @@ function renderSettings() {
|
||||
_syncHeaderUpdateState();
|
||||
container.querySelectorAll('.settings-autosave').forEach((input) => {
|
||||
const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input';
|
||||
input.addEventListener(eventName, () => {
|
||||
input.addEventListener(eventName, async () => {
|
||||
if (input.id === 'languageInput') {
|
||||
setUiLanguage(input.value);
|
||||
syncLanguagePicker(input.value);
|
||||
}
|
||||
if (input.id === 'deleteSourceAfterSuccessfulUploadInput' && input.checked) {
|
||||
const confirmed = await showAppConfirm({
|
||||
title: 'Quelldateien dauerhaft löschen?',
|
||||
message: 'Nach einem vollständigen Upload zu allen ausgewählten Hostern wird die Originaldatei ohne Papierkorb endgültig von diesem PC gelöscht.',
|
||||
confirmText: 'Dauerhaftes Löschen aktivieren',
|
||||
danger: true
|
||||
});
|
||||
if (!confirmed) input.checked = false;
|
||||
}
|
||||
markSettingsDirty();
|
||||
});
|
||||
});
|
||||
@@ -4847,6 +4908,7 @@ async function performSaveSettings(options = {}) {
|
||||
parallelUploadCount: elInt('parallelUploadCountInput', cur.parallelUploadCount ?? 0, 0, 0, 100),
|
||||
scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads),
|
||||
removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone),
|
||||
deleteSourceAfterSuccessfulUpload: elChk('deleteSourceAfterSuccessfulUploadInput', !!cur.deleteSourceAfterSuccessfulUpload),
|
||||
showDropTarget: elChk('showDropTargetInput', !!cur.showDropTarget),
|
||||
globalMaxSpeedKbs: (() => {
|
||||
const el = document.getElementById('globalMaxSpeedMbsInput');
|
||||
|
||||
+138
-1
@@ -189,6 +189,12 @@
|
||||
['Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste.', 'Successfully uploaded files are automatically removed from the upload list.'],
|
||||
['Warteschlange beim Start wiederherstellen', 'Restore queue on startup'],
|
||||
['Noch nicht abgeschlossene Uploads werden beim nächsten Programmstart erneut angezeigt.', 'Incomplete uploads are shown again the next time the application starts.'],
|
||||
['Quelldateien', 'Source files'],
|
||||
['Quelldatei nach vollständigem Upload dauerhaft löschen', 'Permanently delete source file after complete upload'],
|
||||
['Löscht die Originaldatei endgültig, sobald alle dafür ausgewählten Hoster erfolgreich abgeschlossen sind. Der Papierkorb wird nicht verwendet.', 'Permanently deletes the original file after all selected hosts have completed successfully. The Recycle Bin is not used.'],
|
||||
['Quelldateien dauerhaft löschen?', 'Permanently delete source files?'],
|
||||
['Nach einem vollständigen Upload zu allen ausgewählten Hostern wird die Originaldatei ohne Papierkorb endgültig von diesem PC gelöscht.', 'After a complete upload to all selected hosts, the original file is permanently deleted from this PC without using the Recycle Bin.'],
|
||||
['Dauerhaftes Löschen aktivieren', 'Enable permanent deletion'],
|
||||
['Hoster-Einstellungen', 'Host settings'],
|
||||
['Upload-Einstellungen', 'Upload settings'],
|
||||
['Erfolgreiche Links in fileuploader.log.', 'Successful links in fileuploader.log.'],
|
||||
@@ -412,6 +418,109 @@
|
||||
['Prüfen…', 'Checking…'],
|
||||
['Prüfung fehlgeschlagen', 'Check failed'],
|
||||
['Prüfung oder Speichern fehlgeschlagen', 'Check or save failed'],
|
||||
['Einstellungen konnten vor dem Update nicht gespeichert werden', 'Settings could not be saved before the update'],
|
||||
['Das Update wurde nicht gestartet, weil die Einstellungen vor dem Beenden nicht gespeichert werden konnten', 'The update was not started because the settings could not be saved before quitting'],
|
||||
['Login ok, Upload-Seite bereit', 'Login successful, upload page ready'],
|
||||
['Login oder API Key fehlt', 'Login or API key is missing'],
|
||||
['Account-Check lieferte kein gültiges JSON', 'Account check did not return valid JSON'],
|
||||
['Account-Check fehlgeschlagen', 'Account check failed'],
|
||||
['Upload-Server-Check lieferte kein gültiges JSON', 'Upload server check did not return valid JSON'],
|
||||
['API Key gültig, Upload-Server verfügbar', 'API key is valid, upload server available'],
|
||||
['API Key gültig, aktuell kein Server von API (Uploader nutzt Fallback)', 'API key is valid, but the API currently provides no server (the uploader uses a fallback)'],
|
||||
['API Key gültig, Upload-Server aktuell nicht geliefert', 'API key is valid, but the API currently provides no upload server'],
|
||||
['Username oder Passwort fehlt', 'Username or password is missing'],
|
||||
['Upload-URL wurde nicht erkannt', 'Upload URL was not recognized'],
|
||||
['API Key gültig, aktuell kein Server verfügbar', 'API key is valid, but no server is currently available'],
|
||||
['API Key ungültig oder Server nicht erreichbar', 'API key is invalid or the server is unreachable'],
|
||||
['Login ok, aber Upload-Seite liefert kein CSRF-Token', 'Login successful, but the upload page did not provide a CSRF token'],
|
||||
['API Key fehlt', 'API key is missing'],
|
||||
['API lieferte kein gültiges JSON', 'API did not return valid JSON'],
|
||||
['API Key gültig', 'API key is valid'],
|
||||
['Clouddrop Auth fehlgeschlagen', 'Clouddrop authentication failed'],
|
||||
['Account-ID fehlt im Check-Payload', 'Account ID is missing from the check payload'],
|
||||
['Kein Health-Check für diesen Hoster', 'No health check is available for this host'],
|
||||
['Health-Check fehlgeschlagen', 'Health check failed'],
|
||||
['Hoster fehlt', 'Host is missing'],
|
||||
['Validierung fehlgeschlagen', 'Validation failed'],
|
||||
['Ein Upload wird bereits ausgeführt oder abgeschlossen', 'An upload is already running or completed'],
|
||||
['Kein gültiger Account für diesen Hoster', 'No valid account is available for this host'],
|
||||
['Keine gültigen Zugangsdaten für die gewählten Hoster.', 'No valid credentials are available for the selected hosts.'],
|
||||
['Unbekannter Fehler', 'Unknown error'],
|
||||
['Kein Log-Pfad gefunden', 'No log path was found'],
|
||||
['Ungültige URL (muss mit http(s):// beginnen)', 'Invalid URL (must start with http(s)://)'],
|
||||
['Backup-Datei ist zu groß oder ungültig', 'The backup file is too large or invalid'],
|
||||
['Ungültiges Backup-Format', 'Invalid backup format'],
|
||||
['Keine gültige .mhu Backup-Datei', 'Not a valid .mhu backup file'],
|
||||
['Falsches Passwort oder beschädigte Datei', 'Incorrect password or damaged file'],
|
||||
['Dieses Backup wurde mit einem Passwort verschlüsselt', 'This backup was encrypted with a password'],
|
||||
['Die Verlaufsdatei ist beschädigt und wurde nicht verändert', 'The history file is damaged and was not changed'],
|
||||
['Verlauf und Aufbewahrung konnten nicht konsistent gespeichert werden', 'History and retention could not be saved consistently'],
|
||||
['Kein Ordnerpfad angegeben', 'No folder path was provided'],
|
||||
['Clouddrop: Datei ist leer', 'Clouddrop: The file is empty'],
|
||||
['Clouddrop: Keine fileId in Upload-Antwort', 'Clouddrop: No fileId in the upload response'],
|
||||
['Clouddrop: Keine sessionId von /upload/init', 'Clouddrop: No sessionId from /upload/init'],
|
||||
['Doodstream: sess_id nicht gefunden nach Login', 'Doodstream: sess_id was not found after login'],
|
||||
['Kein Upload-Server erhalten. API-Key prüfen.', 'No upload server was returned. Check the API key.'],
|
||||
['Online-Sicherung enthält keine gültigen Einstellungen', 'The online backup does not contain valid settings'],
|
||||
['Online-Sicherungen benötigen eine sichere HTTPS-Verbindung', 'Online backups require a secure HTTPS connection'],
|
||||
['Online-Sicherungsdienst antwortet nicht', 'The online backup service is not responding'],
|
||||
['Online-Sicherungsdienst ist nicht erreichbar', 'The online backup service is unavailable'],
|
||||
['Antwort des Online-Sicherungsdienstes ist zu groß', 'The response from the online backup service is too large'],
|
||||
['Online-Sicherungsschlüssel ist beschädigt', 'The online backup key is damaged'],
|
||||
['Einstellungen sind für eine Online-Sicherung zu groß', 'The settings are too large for an online backup'],
|
||||
['Online-Sicherung ist beschädigt', 'The online backup is damaged'],
|
||||
['Online-Sicherung konnte nicht entschlüsselt werden oder ist beschädigt', 'The online backup could not be decrypted or is damaged'],
|
||||
['Online-Sicherung konnte nicht gespeichert werden', 'Online backup could not be saved'],
|
||||
['Online-Sicherung wurde nicht gefunden', 'Online backup was not found'],
|
||||
['Online-Sicherung konnte nicht geladen werden', 'Online backup could not be loaded'],
|
||||
['Online-Sicherungsdienst hat ungültige Daten geliefert', 'The online backup service returned invalid data'],
|
||||
['Online-Sicherung konnte nicht gelöscht werden', 'Online backup could not be deleted'],
|
||||
['Backup hat eine ungültige Struktur', 'The backup has an invalid structure'],
|
||||
['Einstellungen werden bereits importiert', 'Settings are already being imported'],
|
||||
['Während laufender Uploads können keine Einstellungen importiert werden', 'Settings cannot be imported while uploads are running'],
|
||||
['Online-Sicherungsschlüssel ist ungültig', 'The online backup key is invalid'],
|
||||
['Ein Update wird bereits vorbereitet', 'An update is already being prepared'],
|
||||
['Die Anwendung ist noch nicht bereit, das Update sicher zu installieren', 'The application is not yet ready to install the update safely'],
|
||||
['Update abgebrochen', 'Update canceled'],
|
||||
['Update-Asset unvollständig (URL oder Name fehlt)', 'The update asset is incomplete (URL or name is missing)'],
|
||||
['Heruntergeladene Datei ist keine gültige EXE', 'The downloaded file is not a valid EXE'],
|
||||
['SHA-512 Prüfung fehlgeschlagen', 'SHA-512 verification failed'],
|
||||
['Vorbereitetes Update ist unvollständig', 'The prepared update is incomplete'],
|
||||
['Kein Setup-Asset im Release gefunden', 'No setup asset was found in the release'],
|
||||
['Download hängt — seit 45 s keine Daten (Netzwerk/Server überlastet). Bitte laufende Uploads stoppen und erneut versuchen.', 'The download stalled because no data was received for 45 seconds. Stop active uploads and try again.'],
|
||||
['Datei nicht gefunden', 'File not found'],
|
||||
['Netzwerkfehler', 'Network error'],
|
||||
['Bekanntes Größen-Limit auf diesem Account (frühere verdächtige Ablehnung)', 'Known size limit on this account (previous suspicious rejection)'],
|
||||
['Ablehnung verdächtig - Versuch auf anderem Account', 'Suspicious rejection - trying another account'],
|
||||
['Vidmoly: /api/upload/config lieferte kein JSON — evtl. nicht eingeloggt?', 'Vidmoly: /api/upload/config did not return JSON — you may not be signed in'],
|
||||
['Vidmoly: /api/upload/config unvollständig (sess_id/upload_url fehlt)', 'Vidmoly: /api/upload/config is incomplete (sess_id/upload_url is missing)'],
|
||||
['Vidmoly VM API lieferte kein JSON', 'The Vidmoly VM API did not return JSON'],
|
||||
['VOE Login: CSRF-Token nicht gefunden', 'VOE login: CSRF token not found'],
|
||||
['VOE Upload: CSRF-Token nicht gefunden. Bist du eingeloggt?', 'VOE upload: CSRF token not found. Are you signed in?'],
|
||||
['VOE: Kein Upload-Server erhalten von delivery-node', 'VOE: No upload server was returned by delivery-node'],
|
||||
['VOE Upload: Kein file_code in der Antwort gefunden', 'VOE upload: No file_code was found in the response'],
|
||||
['Nicht gespeicherte laufende Aktionen werden beendet.', 'Unsaved running actions will be stopped.'],
|
||||
['Ausgewählte Einträge entfernen?', 'Remove selected entries?'],
|
||||
['Export fehlgeschlagen', 'Export failed'],
|
||||
['Backup exportiert', 'Backup exported'],
|
||||
['Online-Backup importiert', 'Online backup imported'],
|
||||
['Passwort', 'Password'],
|
||||
['Backup importiert', 'Backup imported'],
|
||||
['Import fehlgeschlagen', 'Import failed'],
|
||||
['Upload-Start fehlgeschlagen', 'Failed to start upload'],
|
||||
['erneut versuchbar', 'retryable'],
|
||||
['manuell', 'manual'],
|
||||
['Abgebrochen.', 'Canceled.'],
|
||||
['API-Token neu erzeugen?', 'Generate a new API token?'],
|
||||
['Der bisherige Token wird sofort ungültig. Verbundene Clients müssen den neuen Token verwenden.', 'The current token will become invalid immediately. Connected clients must use the new token.'],
|
||||
['Neu erzeugen', 'Generate new'],
|
||||
['Verbindungs-Code neu erzeugen?', 'Generate a new connection code?'],
|
||||
['Der bisherige Diagnose-Code wird sofort ungültig.', 'The current diagnostics code will become invalid immediately.'],
|
||||
['Aktivieren', 'Enable'],
|
||||
['Bitte den OTP-Code eingeben.', 'Enter the OTP code.'],
|
||||
['Login fehlgeschlagen', 'Login failed'],
|
||||
['Aufbewahrung ändern?', 'Change retention?'],
|
||||
['Update fehlgeschlagen', 'Update failed'],
|
||||
['Speichere aktuelle Einstellungen…', 'Saving current settings…'],
|
||||
['Speichern vor dem Beenden hat zu lange gedauert', 'Saving before quitting took too long'],
|
||||
['Stoppt nach aktiven Uploads...', 'Stopping after active uploads...'],
|
||||
@@ -482,6 +591,34 @@
|
||||
const patterns = target === 'en'
|
||||
? [
|
||||
[/^Update v(.+) verfügbar$/, 'Update v$1 available'],
|
||||
[/^Login ok, Upload-Form bereit \(Dateifeld: (.+)\)$/, 'Login successful, upload form ready (file field: $1)'],
|
||||
[/^Klartext-Backup ist kein gültiges JSON: (.+)$/, 'Plain JSON backup is not valid JSON: $1'],
|
||||
[/^Export fehlgeschlagen: (.+)$/, 'Export failed: $1'],
|
||||
[/^Import fehlgeschlagen: (.+)$/, 'Import failed: $1'],
|
||||
[/^Initialisierung fehlgeschlagen: (.+)$/, 'Initialization failed: $1'],
|
||||
[/^Quelldatei-Schutz konnte nicht vorbereitet werden: (.+)$/, 'Source file protection could not be prepared: $1'],
|
||||
[/^Clouddrop: API-Antwort war kein JSON (.+)$/, 'Clouddrop: API response was not JSON $1'],
|
||||
[/^Clouddrop: Datei nicht lesbar: (.+)$/, 'Clouddrop: File cannot be read: $1'],
|
||||
[/^Doodstream: konnte Upload-Server nicht ermitteln \(Endpoint geändert\?\)\. (.+)$/, 'Doodstream: Could not determine the upload server (endpoint changed?). $1'],
|
||||
[/^Doodstream Upload fehlgeschlagen: (.+)$/, 'Doodstream upload failed: $1'],
|
||||
[/^Doodstream Upload: kein Filecode — Server gab leeren Link zurück (.+)$/, 'Doodstream upload: No file code — the server returned an empty link $1'],
|
||||
[/^Doodstream lehnt Datei ab \(Server-Status: (.+)\)\. CDN=(.+)$/, 'Doodstream rejected the file (server status: $1). CDN=$2'],
|
||||
[/^Doodstream Upload: Redirect-Antwort ungültig \((.+)\)$/, 'Doodstream upload: Invalid redirect response ($1)'],
|
||||
[/^Doodstream Upload: Keine gültige Antwort \(Body: (.+)\)$/, 'Doodstream upload: No valid response (body: $1)'],
|
||||
[/^Upload zu (.+) wurde vom Server abgelehnt\.$/, 'The upload to $1 was rejected by the server.'],
|
||||
[/^Upload zu (.+) lieferte keine file_code-Antwort \(Payload: (.+)\)$/, 'The upload to $1 returned no file_code response (payload: $2)'],
|
||||
[/^Byse lehnte Datei ab: (.+)$/, 'Byse rejected the file: $1'],
|
||||
[/^API-Antwort war kein JSON (.+)$/, 'The API response was not JSON $1'],
|
||||
[/^Kein Upload-Server erhalten: (.+)$/, 'No upload server was returned: $1'],
|
||||
[/^Upload-Antwort von (.+) war kein JSON (.+)$/, 'The upload response from $1 was not JSON $2'],
|
||||
[/^Vidmoly Login fehlgeschlagen: (.+)$/, 'Vidmoly login failed: $1'],
|
||||
[/^Vidmoly Upload abgelehnt: (.+)$/, 'Vidmoly upload rejected: $1'],
|
||||
[/^Vidmoly Upload-Ergebnis: (.+)$/, 'Vidmoly upload result: $1'],
|
||||
[/^VOE Login fehlgeschlagen: (.+)$/, 'VOE login failed: $1'],
|
||||
[/^VOE Upload-Fehler: (.+)$/, 'VOE upload error: $1'],
|
||||
[/^VOE: Upload-Server Antwort war kein JSON: (.+)$/, 'VOE: The upload server response was not JSON: $1'],
|
||||
[/^Download fehlgeschlagen: (.+)$/, 'Download failed: $1'],
|
||||
[/^Update-Server Antwort war kein JSON (.+)$/, 'Update server response was not JSON $1'],
|
||||
[/^(\d+) Links kopiert$/, '$1 links copied'],
|
||||
[/^(\d+) Link kopiert$/, '$1 link copied'],
|
||||
[/^Wirklich alle (\d+) Links aus diesem Panel entfernen\?$/, 'Remove all $1 links from this panel?'],
|
||||
@@ -536,7 +673,7 @@
|
||||
[/^Fertig:$/, 'Completed:'],
|
||||
[/^Gespeichert: (.+) \((.+) KB\)$/, 'Saved: $1 ($2 KB)'],
|
||||
[/^Import übernommen\. Warteschlange konnte nicht vollständig gespeichert werden: (.+)$/, 'Import applied. The queue could not be saved completely: $1'],
|
||||
[/^Jobs konnten nicht hinzugefuegt werden: (.+)$/, 'Jobs could not be added: $1'],
|
||||
[/^Jobs konnten nicht hinzugefügt werden: (.+)$/, 'Jobs could not be added: $1'],
|
||||
[/^Links kopieren \((\d+)\)$/, 'Copy links ($1)'],
|
||||
[/^Log-Pfad nicht beschreibbar — schreibe nach: (.+)$/, 'Log path is not writable — writing to: $1'],
|
||||
[/^Login: (.+)$/, 'Login: $1'],
|
||||
|
||||
@@ -653,6 +653,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../lib/source-cleanup-policy.js"></script>
|
||||
<script src="../lib/queue-prune.js"></script>
|
||||
<script src="../lib/queue-dedup.js"></script>
|
||||
<script src="../lib/log-mode.js"></script>
|
||||
|
||||
+169
-7
@@ -1887,12 +1887,6 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
transition: opacity .16s ease;
|
||||
}
|
||||
|
||||
.upload-speed-sparkline.is-hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.upload-speed-sparkline canvas {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
@@ -3272,6 +3266,15 @@ input[type="checkbox"] {
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.settings-option.source-delete-option {
|
||||
border-color: color-mix(in srgb, var(--danger) 72%, var(--border));
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 10%, transparent);
|
||||
}
|
||||
|
||||
.settings-option.source-delete-option:focus-within {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.settings-option-description,
|
||||
.hint {
|
||||
color: var(--text-dim);
|
||||
@@ -3292,6 +3295,165 @@ input[type="checkbox"] {
|
||||
grid-template-columns: minmax(140px, auto) minmax(180px, 1fr) auto;
|
||||
}
|
||||
|
||||
#settings-view {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#settings-view :is(input, textarea) {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.settings-header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.settings-header .settings-hint {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-search-wrap > label,
|
||||
.settings-section-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.settings-search-control,
|
||||
#settingsSearchInput {
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
#settingsSearchInput {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-nav-button {
|
||||
min-height: 42px;
|
||||
padding: 9px 11px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-sidebar-status .save-feedback {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
|
||||
.settings-subpage {
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.settings-page-header {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.settings-page-header h3 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.settings-page-header p {
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.settings-section-label {
|
||||
margin-top: 28px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-subpage .settings-row {
|
||||
min-height: 64px;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.settings-subpage .settings-row > label {
|
||||
min-width: 210px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-subpage .settings-row > .hint {
|
||||
padding-left: 220px;
|
||||
}
|
||||
|
||||
.settings-subpage .settings-row-wide > .hint {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.settings-subpage :is(.key-input, .hs-input:not([type="checkbox"])) {
|
||||
padding: 8px 11px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-subpage .automation-retry-row {
|
||||
grid-template-columns: 210px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.settings-subpage .automation-retry-row > label {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.settings-option {
|
||||
min-height: 70px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.settings-option-copy {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.settings-option-copy label,
|
||||
.program-update-title,
|
||||
.online-backup-key-row label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-option-description,
|
||||
.program-update-description,
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.settings-option > input[type="checkbox"],
|
||||
.settings-subpage .settings-grid-mini .checkbox-row input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.settings-subpage .settings-grid-mini .settings-row.checkbox-row {
|
||||
min-height: 62px;
|
||||
}
|
||||
|
||||
.program-update-row {
|
||||
min-height: 90px !important;
|
||||
}
|
||||
|
||||
.language-option {
|
||||
min-height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-hoster-pointer,
|
||||
.online-backup-status,
|
||||
.settings-empty {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.online-backup-panel h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.online-backup-panel p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#settings-view .btn {
|
||||
min-height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.accounts-main,
|
||||
.history-main {
|
||||
padding: 0;
|
||||
@@ -3708,7 +3870,7 @@ input[type="checkbox"] {
|
||||
|
||||
.settings-nav-button {
|
||||
padding: 7px 8px;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
|
||||
Reference in New Issue
Block a user