Add include and exclude filename conditions across every new import path, show accepted and excluded counts before destination selection, and keep restored queues unchanged. Refine the settings layout and reorder upload telemetry for faster status reading.
This commit is contained in:
@@ -8,20 +8,22 @@ Multi Hoster Uploader is a Windows desktop application for sending file batches
|
|||||||
|
|
||||||
Download the current Setup or Portable build from the [latest GitHub release](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases/latest).
|
Download the current Setup or Portable build from the [latest GitHub release](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases/latest).
|
||||||
|
|
||||||
The latest public release is version 2.1.20. Use the release page for the executables and the full English changelog.
|
The latest public release is version 2.1.21. Use the release page for the executables and the full English changelog.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Upload workspace
|
### Upload workspace
|
||||||
|
|
||||||
- Add individual files, complete folders, or files by drag and drop.
|
- Add individual files, complete folders, or files by drag and drop.
|
||||||
|
- Filter new imports by file name with reusable include or exclude conditions before upload jobs are created.
|
||||||
|
- Review how many selected files were accepted or excluded before choosing upload destinations.
|
||||||
- Build one job per selected file and destination.
|
- Build one job per selected file and destination.
|
||||||
- Upload to several supported hosts from the same queue.
|
- Upload to several supported hosts from the same queue.
|
||||||
- Filter the workspace by all, active, queued, completed, or failed jobs.
|
- Filter the workspace by all, active, queued, completed, or failed jobs.
|
||||||
- Search and filter queue entries by file name, host, and status.
|
- Search and filter queue entries by file name, host, and status.
|
||||||
- Open per-upload diagnostics with the selected account, retry count, and safe error details.
|
- Open per-upload diagnostics with the selected account, retry count, and safe error details.
|
||||||
- Track status, smoothly interpolated progress, transferred size, speed, and the selected host account.
|
- Track status, smoothly interpolated progress, transferred size, speed, and the selected host account.
|
||||||
- Read total, remaining, running, completed, and failed upload activity from the persistent sidebar telemetry.
|
- Read remaining, total, running, connection, completed, and failed upload activity from the persistent sidebar telemetry.
|
||||||
- Follow current upload speed in the sidebar and the synchronized header graph.
|
- Follow current upload speed in the sidebar and the synchronized header graph.
|
||||||
- Reorder selected jobs, start selected jobs, retry finished jobs, or stop active work.
|
- Reorder selected jobs, start selected jobs, retry finished jobs, or stop active work.
|
||||||
- Copy completed links individually or together.
|
- Copy completed links individually or together.
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ const DEFAULTS = {
|
|||||||
lastBrowseDirectory: '',
|
lastBrowseDirectory: '',
|
||||||
removeFromQueueOnDone: false,
|
removeFromQueueOnDone: false,
|
||||||
deleteSourceAfterSuccessfulUpload: false,
|
deleteSourceAfterSuccessfulUpload: false,
|
||||||
|
filenameFilter: {
|
||||||
|
enabled: false,
|
||||||
|
action: 'include',
|
||||||
|
matchMode: 'all',
|
||||||
|
conditions: []
|
||||||
|
},
|
||||||
showDropTarget: false,
|
showDropTarget: false,
|
||||||
globalMaxSpeedKbs: 0, // 0 = unlimited global speed
|
globalMaxSpeedKbs: 0, // 0 = unlimited global speed
|
||||||
pendingQueue: null,
|
pendingQueue: null,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
(function initFilenameFilter(root, factory) {
|
||||||
|
const api = factory();
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||||
|
if (root) root.FilenameFilter = api;
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis, function createFilenameFilter() {
|
||||||
|
function normalizeFilenameFilter(value) {
|
||||||
|
const source = value && typeof value === 'object' ? value : {};
|
||||||
|
const conditions = Array.isArray(source.conditions)
|
||||||
|
? source.conditions.flatMap(condition => {
|
||||||
|
if (!condition || typeof condition !== 'object') return [];
|
||||||
|
const text = String(condition.value ?? '').trim();
|
||||||
|
if (!text) return [];
|
||||||
|
return [{
|
||||||
|
operator: condition.operator === 'notContains' ? 'notContains' : 'contains',
|
||||||
|
value: text
|
||||||
|
}];
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
return {
|
||||||
|
enabled: source.enabled === true,
|
||||||
|
action: source.action === 'exclude' ? 'exclude' : 'include',
|
||||||
|
matchMode: source.matchMode === 'any' ? 'any' : 'all',
|
||||||
|
conditions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilename(entry) {
|
||||||
|
if (entry && typeof entry === 'object' && entry.name) return String(entry.name);
|
||||||
|
const source = entry && typeof entry === 'object' ? entry.path : entry;
|
||||||
|
return String(source ?? '').split(/[\\/]/).pop() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateFilenameFilter(filename, value) {
|
||||||
|
const filter = normalizeFilenameFilter(value);
|
||||||
|
const active = filter.enabled && filter.conditions.length > 0;
|
||||||
|
if (!active) return { accepted: true, matched: false, active, filter };
|
||||||
|
const normalizedName = String(filename ?? '').toLowerCase();
|
||||||
|
const results = filter.conditions.map(condition => {
|
||||||
|
const contains = normalizedName.includes(condition.value.toLowerCase());
|
||||||
|
return condition.operator === 'notContains' ? !contains : contains;
|
||||||
|
});
|
||||||
|
const matched = filter.matchMode === 'any' ? results.some(Boolean) : results.every(Boolean);
|
||||||
|
const accepted = filter.action === 'exclude' ? !matched : matched;
|
||||||
|
return { accepted, matched, active, filter };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilenameFilter(entries, value) {
|
||||||
|
const filter = normalizeFilenameFilter(value);
|
||||||
|
const accepted = [];
|
||||||
|
const excluded = [];
|
||||||
|
for (const entry of Array.isArray(entries) ? entries : []) {
|
||||||
|
const evaluation = evaluateFilenameFilter(getFilename(entry), filter);
|
||||||
|
if (evaluation.accepted) accepted.push(entry);
|
||||||
|
else excluded.push(entry);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total: accepted.length + excluded.length,
|
||||||
|
accepted,
|
||||||
|
excluded,
|
||||||
|
active: filter.enabled && filter.conditions.length > 0,
|
||||||
|
filter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
normalizeFilenameFilter,
|
||||||
|
evaluateFilenameFilter,
|
||||||
|
applyFilenameFilter
|
||||||
|
};
|
||||||
|
});
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.1.20",
|
"version": "2.1.21",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.1.20",
|
"version": "2.1.21",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"undici": "^7.29.0",
|
"undici": "^7.29.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.1.20",
|
"version": "2.1.21",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+177
-33
@@ -651,12 +651,14 @@ async function init() {
|
|||||||
const name = p.split('\\').pop().split('/').pop();
|
const name = p.split('\\').pop().split('/').pop();
|
||||||
newFiles.push({ path: p, name, size: null });
|
newFiles.push({ path: p, name, size: null });
|
||||||
}
|
}
|
||||||
if (newFiles.length > 0) {
|
const admitted = admitFilenameFilter(newFiles);
|
||||||
const newPaths = new Set(newFiles.map(f => f.path));
|
if (admitted.accepted.length > 0) {
|
||||||
|
const newPaths = new Set(admitted.accepted.map(f => f.path));
|
||||||
clearDedupKeysForPaths(newPaths);
|
clearDedupKeysForPaths(newPaths);
|
||||||
selectedFiles.push(...newFiles);
|
selectedFiles.push(...admitted.accepted);
|
||||||
buildQueuePreview();
|
buildQueuePreview();
|
||||||
updateUploadView();
|
updateUploadView();
|
||||||
|
if (admitted.active && admitted.excluded.length > 0) showFilenameFilterResult(admitted);
|
||||||
if (fm.autoStart && !uploading && !healthCheckRunning) {
|
if (fm.autoStart && !uploading && !healthCheckRunning) {
|
||||||
startUpload();
|
startUpload();
|
||||||
} else if (uploading) {
|
} else if (uploading) {
|
||||||
@@ -666,6 +668,8 @@ async function init() {
|
|||||||
await startSelectedUpload(newJobs);
|
await startSelectedUpload(newJobs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (admitted.active && admitted.total > 0) {
|
||||||
|
showFilenameFilterResult(admitted);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No pre-selected hosters: open modal
|
// No pre-selected hosters: open modal
|
||||||
@@ -1260,6 +1264,12 @@ function renderHosterModal() {
|
|||||||
function openHosterModal() {
|
function openHosterModal() {
|
||||||
syncSelectedUploadHosters();
|
syncSelectedUploadHosters();
|
||||||
renderHosterModal();
|
renderHosterModal();
|
||||||
|
const description = document.getElementById('hosterModalDescription');
|
||||||
|
if (description) {
|
||||||
|
description.textContent = _pendingImportSummary
|
||||||
|
? formatFilenameFilterResult(_pendingImportSummary)
|
||||||
|
: localizeUiText('Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.');
|
||||||
|
}
|
||||||
modalController.open('hosterModal', {
|
modalController.open('hosterModal', {
|
||||||
initialFocus: '#cancelHosterModalBtn',
|
initialFocus: '#cancelHosterModalBtn',
|
||||||
fallbackFocus: '#addFilesBtn',
|
fallbackFocus: '#addFilesBtn',
|
||||||
@@ -1298,10 +1308,12 @@ async function applyHosterSelection() {
|
|||||||
updateUploadView();
|
updateUploadView();
|
||||||
persistQueueStateSoon(true); // immediate persist after adding files
|
persistQueueStateSoon(true); // immediate persist after adding files
|
||||||
closeHosterModal();
|
closeHosterModal();
|
||||||
|
_pendingImportSummary = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelHosterModal() {
|
function cancelHosterModal() {
|
||||||
_pendingFiles = [];
|
_pendingFiles = [];
|
||||||
|
_pendingImportSummary = null;
|
||||||
closeHosterModal();
|
closeHosterModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1538,9 +1550,37 @@ function setupDragDrop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _pendingFiles = []; // Files waiting for hoster modal confirmation
|
let _pendingFiles = []; // Files waiting for hoster modal confirmation
|
||||||
|
let _pendingImportSummary = null;
|
||||||
|
|
||||||
let _addingDropped = false;
|
let _addingDropped = false;
|
||||||
|
|
||||||
|
function admitFilenameFilter(files) {
|
||||||
|
return window.FilenameFilter.applyFilenameFilter(files, config?.globalSettings?.filenameFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePendingImportSummary(result) {
|
||||||
|
if (!result.active) {
|
||||||
|
if (!_pendingImportSummary) _pendingImportSummary = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = _pendingImportSummary || { total: 0, accepted: 0, excluded: 0 };
|
||||||
|
_pendingImportSummary = {
|
||||||
|
total: current.total + result.total,
|
||||||
|
accepted: current.accepted + result.accepted.length,
|
||||||
|
excluded: current.excluded + result.excluded.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFilenameFilterResult(result) {
|
||||||
|
const accepted = Array.isArray(result.accepted) ? result.accepted.length : Number(result.accepted) || 0;
|
||||||
|
const excluded = Array.isArray(result.excluded) ? result.excluded.length : Number(result.excluded) || 0;
|
||||||
|
return localizeUiText(`${accepted} von ${result.total} Dateien werden hinzugefügt. ${excluded} durch den Dateinamenfilter ausgeschlossen.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFilenameFilterResult(result) {
|
||||||
|
showCopyToast(formatFilenameFilterResult(result), 6500);
|
||||||
|
}
|
||||||
|
|
||||||
async function addDropTargetEntries(entries) {
|
async function addDropTargetEntries(entries) {
|
||||||
const files = [];
|
const files = [];
|
||||||
for (const entry of Array.isArray(entries) ? entries : []) {
|
for (const entry of Array.isArray(entries) ? entries : []) {
|
||||||
@@ -1563,12 +1603,7 @@ async function addDroppedFiles(fileList) {
|
|||||||
_addingDropped = true;
|
_addingDropped = true;
|
||||||
try {
|
try {
|
||||||
const files = Array.from(fileList);
|
const files = Array.from(fileList);
|
||||||
const existingPaths = new Set([
|
const entries = [];
|
||||||
...selectedFiles.map(f => f.path),
|
|
||||||
..._pendingFiles.map(f => f.path)
|
|
||||||
]);
|
|
||||||
const newFiles = [];
|
|
||||||
let duplicateCount = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
let filePath = '';
|
let filePath = '';
|
||||||
@@ -1583,14 +1618,9 @@ async function addDroppedFiles(fileList) {
|
|||||||
for (const fp of folderFiles) {
|
for (const fp of folderFiles) {
|
||||||
const p = typeof fp === 'string' ? fp : (fp && fp.path);
|
const p = typeof fp === 'string' ? fp : (fp && fp.path);
|
||||||
if (!p) continue;
|
if (!p) continue;
|
||||||
if (existingPaths.has(p)) {
|
|
||||||
duplicateCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const name = typeof fp === 'string' ? p.split('\\').pop().split('/').pop() : (fp.name || p.split('\\').pop().split('/').pop());
|
const name = typeof fp === 'string' ? p.split('\\').pop().split('/').pop() : (fp.name || p.split('\\').pop().split('/').pop());
|
||||||
const size = typeof fp === 'string' ? null : (fp.size || 0);
|
const size = typeof fp === 'string' ? null : (fp.size || 0);
|
||||||
newFiles.push({ path: p, name, size });
|
entries.push({ path: p, name, size });
|
||||||
existingPaths.add(p);
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1599,20 +1629,9 @@ async function addDroppedFiles(fileList) {
|
|||||||
|
|
||||||
// Regular file
|
// Regular file
|
||||||
const fileName = file.name || '';
|
const fileName = file.name || '';
|
||||||
if (!existingPaths.has(filePath)) {
|
entries.push({ path: filePath, name: fileName, size: file.size });
|
||||||
newFiles.push({ path: filePath, name: fileName, size: file.size });
|
|
||||||
existingPaths.add(filePath);
|
|
||||||
} else {
|
|
||||||
duplicateCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newFiles.length > 0) {
|
|
||||||
_pendingFiles.push(...newFiles);
|
|
||||||
openHosterModal();
|
|
||||||
} else if (duplicateCount > 0) {
|
|
||||||
showCopyToast('Auswahl ist bereits in den Upload-Aufträgen.');
|
|
||||||
}
|
}
|
||||||
|
addPathsToQueue(entries);
|
||||||
} finally {
|
} finally {
|
||||||
_addingDropped = false;
|
_addingDropped = false;
|
||||||
}
|
}
|
||||||
@@ -1648,11 +1667,15 @@ function addPathsToQueue(paths) {
|
|||||||
newFiles.push({ path: p, name, size });
|
newFiles.push({ path: p, name, size });
|
||||||
if (size === null || size === undefined || size === 0) pendingSizeFetch.push(p);
|
if (size === null || size === undefined || size === 0) pendingSizeFetch.push(p);
|
||||||
}
|
}
|
||||||
if (newFiles.length > 0) {
|
const admitted = admitFilenameFilter(newFiles);
|
||||||
_pendingFiles.push(...newFiles);
|
if (admitted.accepted.length > 0) {
|
||||||
|
const acceptedPaths = new Set(admitted.accepted.map(file => file.path));
|
||||||
|
const acceptedSizeFetch = pendingSizeFetch.filter(filePath => acceptedPaths.has(filePath));
|
||||||
|
_pendingFiles.push(...admitted.accepted);
|
||||||
|
mergePendingImportSummary(admitted);
|
||||||
openHosterModal();
|
openHosterModal();
|
||||||
if (pendingSizeFetch.length > 0 && window.api.getFileSizes) {
|
if (acceptedSizeFetch.length > 0 && window.api.getFileSizes) {
|
||||||
window.api.getFileSizes(pendingSizeFetch).then((sizeMap) => {
|
window.api.getFileSizes(acceptedSizeFetch).then((sizeMap) => {
|
||||||
if (!sizeMap || typeof sizeMap !== 'object') return;
|
if (!sizeMap || typeof sizeMap !== 'object') return;
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const f of _pendingFiles) {
|
for (const f of _pendingFiles) {
|
||||||
@@ -1671,6 +1694,10 @@ function addPathsToQueue(paths) {
|
|||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
} else if (admitted.active && admitted.total > 0) {
|
||||||
|
showFilenameFilterResult(admitted);
|
||||||
|
} else if (Array.isArray(paths) && paths.length > 0) {
|
||||||
|
showCopyToast('Auswahl ist bereits in den Upload-Aufträgen.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4605,6 +4632,79 @@ async function _renderLogPathsList(el) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filenameFilterConditionRowHtml(condition = {}) {
|
||||||
|
const operator = condition.operator === 'notContains' ? 'notContains' : 'contains';
|
||||||
|
return `
|
||||||
|
<div class="filename-filter-condition" data-filename-filter-condition>
|
||||||
|
<div class="filename-filter-rule-field">
|
||||||
|
<span class="filename-filter-rule-label" data-filename-filter-value-label>Dateiname filtern</span>
|
||||||
|
<input type="text" class="key-input settings-autosave" data-filename-filter-value value="${escapeAttr(condition.value || '')}" placeholder="z. B. 720p" aria-label="Dateiname filtern">
|
||||||
|
</div>
|
||||||
|
<div class="filename-filter-rule-field">
|
||||||
|
<span class="filename-filter-rule-label" data-filename-filter-operator-label>Vergleich</span>
|
||||||
|
<select class="hs-input settings-autosave" data-filename-filter-operator aria-label="Dateinamen-Bedingung">
|
||||||
|
<option value="contains" ${operator === 'contains' ? 'selected' : ''}>enthält</option>
|
||||||
|
<option value="notContains" ${operator === 'notContains' ? 'selected' : ''}>enthält nicht</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-xs btn-danger" data-filename-filter-remove aria-label="Bedingung entfernen">Entfernen</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFilenameFilterSettings() {
|
||||||
|
const conditions = Array.from(document.querySelectorAll('[data-filename-filter-condition]')).map(row => ({
|
||||||
|
operator: row.querySelector('[data-filename-filter-operator]')?.value || 'contains',
|
||||||
|
value: row.querySelector('[data-filename-filter-value]')?.value || ''
|
||||||
|
}));
|
||||||
|
return window.FilenameFilter.normalizeFilenameFilter({
|
||||||
|
enabled: document.getElementById('filenameFilterEnabledInput')?.checked === true,
|
||||||
|
action: document.getElementById('filenameFilterActionInput')?.value,
|
||||||
|
matchMode: document.getElementById('filenameFilterMatchModeInput')?.value,
|
||||||
|
conditions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFilenameFilterControls() {
|
||||||
|
const enabled = document.getElementById('filenameFilterEnabledInput')?.checked === true;
|
||||||
|
const panel = document.getElementById('filenameFilterBuilder');
|
||||||
|
if (panel) panel.classList.toggle('disabled', !enabled);
|
||||||
|
const rows = Array.from(document.querySelectorAll('[data-filename-filter-condition]'));
|
||||||
|
document.querySelectorAll('#filenameFilterBuilder select, #filenameFilterBuilder input, #addFilenameFilterConditionBtn').forEach(control => {
|
||||||
|
control.disabled = !enabled;
|
||||||
|
});
|
||||||
|
rows.forEach(row => {
|
||||||
|
const remove = row.querySelector('[data-filename-filter-remove]');
|
||||||
|
if (remove) remove.disabled = !enabled || rows.length === 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireFilenameFilterConditionRow(row) {
|
||||||
|
if (!row || row.dataset.wired === 'true') return;
|
||||||
|
row.dataset.wired = 'true';
|
||||||
|
row.querySelectorAll('.settings-autosave').forEach(control => {
|
||||||
|
const eventName = control.tagName === 'SELECT' ? 'change' : 'input';
|
||||||
|
control.addEventListener(eventName, markSettingsDirty);
|
||||||
|
});
|
||||||
|
row.querySelector('[data-filename-filter-remove]')?.addEventListener('click', () => {
|
||||||
|
row.remove();
|
||||||
|
syncFilenameFilterControls();
|
||||||
|
markSettingsDirty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendFilenameFilterCondition(condition = {}) {
|
||||||
|
const list = document.getElementById('filenameFilterConditions');
|
||||||
|
if (!list) return;
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = filenameFilterConditionRowHtml(condition).trim();
|
||||||
|
const row = template.content.firstElementChild;
|
||||||
|
list.appendChild(row);
|
||||||
|
wireFilenameFilterConditionRow(row);
|
||||||
|
syncFilenameFilterControls();
|
||||||
|
row.querySelector('[data-filename-filter-value]')?.focus();
|
||||||
|
markSettingsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
function renderSettings() {
|
function renderSettings() {
|
||||||
const container = document.getElementById('settingsHosters');
|
const container = document.getElementById('settingsHosters');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
@@ -4613,10 +4713,14 @@ function renderSettings() {
|
|||||||
const configuredAccounts = getAvailableHosters();
|
const configuredAccounts = getAvailableHosters();
|
||||||
const fm = globalSettings.folderMonitor || {};
|
const fm = globalSettings.folderMonitor || {};
|
||||||
const remoteSettings = globalSettings.remote || {};
|
const remoteSettings = globalSettings.remote || {};
|
||||||
|
const filenameFilter = window.FilenameFilter.normalizeFilenameFilter(globalSettings.filenameFilter);
|
||||||
|
const filenameFilterConditions = filenameFilter.conditions.length > 0
|
||||||
|
? filenameFilter.conditions
|
||||||
|
: [{ operator: 'contains', value: '' }];
|
||||||
|
|
||||||
const pageDefinitions = [
|
const pageDefinitions = [
|
||||||
{ id: 'allgemein', label: 'Allgemein', search: 'fenster window vordergrund foreground always on top drop target oberfläche interface updates update aktualisierung version language sprache' },
|
{ id: 'allgemein', label: 'Allgemein', search: 'fenster window vordergrund foreground always on top drop target oberfläche interface updates update aktualisierung version language sprache' },
|
||||||
{ id: 'uploads', label: 'Uploads', search: 'upload queue warteschlange waiting fertig completed completion abschluss entfernen remove parallel geschwindigkeit speed limit fortsetzen resume wiederherstellen restore hoster' },
|
{ id: 'uploads', label: 'Uploads', search: 'upload queue warteschlange waiting fertig completed completion abschluss entfernen remove parallel geschwindigkeit speed limit fortsetzen resume wiederherstellen restore hoster dateiname filename filter enthält contains ausschließen exclude' },
|
||||||
{ id: 'automatik', label: 'Automatik', search: 'automatisch automation automatic retry wiederholen ordner folder monitor überwachen watch dateierweiterungen extensions unterordner subfolders duplikate duplicates' },
|
{ id: 'automatik', label: 'Automatik', search: 'automatisch automation automatic retry wiederholen ordner folder monitor überwachen watch dateierweiterungen extensions unterordner subfolders duplikate duplicates' },
|
||||||
{ id: 'benachrichtigungen', label: 'Benachrichtigungen', search: 'benachrichtigungen notifications webhook discord meldung message ping erwähnung mention batch fertig completed' },
|
{ id: 'benachrichtigungen', label: 'Benachrichtigungen', search: 'benachrichtigungen notifications webhook discord meldung message ping erwähnung mention batch fertig completed' },
|
||||||
{ id: 'logs', label: 'Logs & Support', search: 'log logs protokoll logging debug verbose diagnose diagnostics support paket package datei file ordner folder' },
|
{ id: 'logs', label: 'Logs & Support', search: 'log logs protokoll logging debug verbose diagnose diagnostics support paket package datei file ordner folder' },
|
||||||
@@ -4748,6 +4852,41 @@ function renderSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="checkbox" class="settings-autosave" id="autoStartRestoredQueueInput" ${globalSettings.autoStartRestoredQueue ? 'checked' : ''}>
|
<input type="checkbox" class="settings-autosave" id="autoStartRestoredQueueInput" ${globalSettings.autoStartRestoredQueue ? 'checked' : ''}>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-section-label">Dateinamenfilter</div>
|
||||||
|
<div class="filename-filter-panel">
|
||||||
|
<div class="settings-option filename-filter-toggle">
|
||||||
|
<div class="settings-option-copy">
|
||||||
|
<label for="filenameFilterEnabledInput">Dateinamen beim Hinzufügen filtern</label>
|
||||||
|
<span class="settings-option-description">Prüft neue Dateien aus Auswahl, Ordnern, Drag-and-drop und Ordnerüberwachung, bevor sie in die Upload-Liste gelangen.</span>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" class="settings-autosave" id="filenameFilterEnabledInput" ${filenameFilter.enabled ? 'checked' : ''}>
|
||||||
|
</div>
|
||||||
|
<div class="filename-filter-builder" id="filenameFilterBuilder">
|
||||||
|
<div class="filename-filter-conditions" id="filenameFilterConditions">
|
||||||
|
${filenameFilterConditions.map(filenameFilterConditionRowHtml).join('')}
|
||||||
|
</div>
|
||||||
|
<div class="filename-filter-footer">
|
||||||
|
<button type="button" class="btn btn-xs btn-secondary" id="addFilenameFilterConditionBtn">+ Bedingung</button>
|
||||||
|
<span class="hint">Groß- und Kleinschreibung werden ignoriert. Leere Bedingungen werden nicht gespeichert.</span>
|
||||||
|
</div>
|
||||||
|
<div class="filename-filter-policy">
|
||||||
|
<div class="filename-filter-field">
|
||||||
|
<label for="filenameFilterMatchModeInput">Bedingungen</label>
|
||||||
|
<select class="hs-input settings-autosave" id="filenameFilterMatchModeInput">
|
||||||
|
<option value="all" ${filenameFilter.matchMode === 'all' ? 'selected' : ''}>alle müssen passen</option>
|
||||||
|
<option value="any" ${filenameFilter.matchMode === 'any' ? 'selected' : ''}>mindestens eine muss passen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filename-filter-field">
|
||||||
|
<label for="filenameFilterActionInput">Wenn passend</label>
|
||||||
|
<select class="hs-input settings-autosave" id="filenameFilterActionInput">
|
||||||
|
<option value="include" ${filenameFilter.action === 'include' ? 'selected' : ''}>Dateien hinzufügen</option>
|
||||||
|
<option value="exclude" ${filenameFilter.action === 'exclude' ? 'selected' : ''}>Dateien nicht hinzufügen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="settings-section-label">Quelldateien</div>
|
<div class="settings-section-label">Quelldateien</div>
|
||||||
<div class="settings-option source-delete-option">
|
<div class="settings-option source-delete-option">
|
||||||
<div class="settings-option-copy">
|
<div class="settings-option-copy">
|
||||||
@@ -5247,6 +5386,10 @@ function renderSettings() {
|
|||||||
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
|
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
|
||||||
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
|
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
|
||||||
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', requestUpdateCheck);
|
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', requestUpdateCheck);
|
||||||
|
container.querySelectorAll('[data-filename-filter-condition]').forEach(wireFilenameFilterConditionRow);
|
||||||
|
document.getElementById('addFilenameFilterConditionBtn')?.addEventListener('click', () => appendFilenameFilterCondition());
|
||||||
|
document.getElementById('filenameFilterEnabledInput')?.addEventListener('change', syncFilenameFilterControls);
|
||||||
|
syncFilenameFilterControls();
|
||||||
_syncHeaderUpdateState();
|
_syncHeaderUpdateState();
|
||||||
container.querySelectorAll('.settings-autosave').forEach((input) => {
|
container.querySelectorAll('.settings-autosave').forEach((input) => {
|
||||||
const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input';
|
const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input';
|
||||||
@@ -5393,6 +5536,7 @@ async function performSaveSettings(options = {}) {
|
|||||||
scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads),
|
scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads),
|
||||||
removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone),
|
removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone),
|
||||||
deleteSourceAfterSuccessfulUpload: elChk('deleteSourceAfterSuccessfulUploadInput', !!cur.deleteSourceAfterSuccessfulUpload),
|
deleteSourceAfterSuccessfulUpload: elChk('deleteSourceAfterSuccessfulUploadInput', !!cur.deleteSourceAfterSuccessfulUpload),
|
||||||
|
filenameFilter: readFilenameFilterSettings(),
|
||||||
showDropTarget: elChk('showDropTargetInput', !!cur.showDropTarget),
|
showDropTarget: elChk('showDropTargetInput', !!cur.showDropTarget),
|
||||||
globalMaxSpeedKbs: (() => {
|
globalMaxSpeedKbs: (() => {
|
||||||
const el = document.getElementById('globalMaxSpeedMbsInput');
|
const el = document.getElementById('globalMaxSpeedMbsInput');
|
||||||
|
|||||||
@@ -37,6 +37,27 @@
|
|||||||
['Fensterverhalten, Drop-Target und Programmupdates.', 'Window behavior, drop target, and application updates.'],
|
['Fensterverhalten, Drop-Target und Programmupdates.', 'Window behavior, drop target, and application updates.'],
|
||||||
['Gesamt:', 'Total:'],
|
['Gesamt:', 'Total:'],
|
||||||
['Globales Speed-Limit', 'Global speed limit'],
|
['Globales Speed-Limit', 'Global speed limit'],
|
||||||
|
['Dateinamenfilter', 'Filename filter'],
|
||||||
|
['Dateinamen beim Hinzufügen filtern', 'Filter filenames when adding files'],
|
||||||
|
['Prüft neue Dateien aus Auswahl, Ordnern, Drag-and-drop und Ordnerüberwachung, bevor sie in die Upload-Liste gelangen.', 'Checks new files from selections, folders, drag and drop, and folder monitoring before they enter the upload list.'],
|
||||||
|
['Treffer', 'Matches'],
|
||||||
|
['Wenn passend', 'When matched'],
|
||||||
|
['Bedingungen', 'Conditions'],
|
||||||
|
['nur hinzufügen', 'add only'],
|
||||||
|
['nicht hinzufügen', 'do not add'],
|
||||||
|
['Dateien hinzufügen', 'Add files'],
|
||||||
|
['Dateien nicht hinzufügen', 'Do not add files'],
|
||||||
|
['alle müssen passen', 'all must match'],
|
||||||
|
['mindestens eine muss passen', 'at least one must match'],
|
||||||
|
['Dateinamen-Bedingung', 'Filename condition'],
|
||||||
|
['Vergleich', 'Comparison'],
|
||||||
|
['enthält', 'contains'],
|
||||||
|
['enthält nicht', 'does not contain'],
|
||||||
|
['Dateiname filtern', 'Filter file name'],
|
||||||
|
['z. B. 720p', 'e.g. 720p'],
|
||||||
|
['Bedingung entfernen', 'Remove condition'],
|
||||||
|
['+ Bedingung', '+ Condition'],
|
||||||
|
['Groß- und Kleinschreibung werden ignoriert. Leere Bedingungen werden nicht gespeichert.', 'Matching ignores letter case. Empty conditions are not saved.'],
|
||||||
['Hauptbereiche', 'Main sections'],
|
['Hauptbereiche', 'Main sections'],
|
||||||
['Hinweis', 'Notice'],
|
['Hinweis', 'Notice'],
|
||||||
['Hoster-Limits automatisch hochskalieren', 'Automatically scale host limits'],
|
['Hoster-Limits automatisch hochskalieren', 'Automatically scale host limits'],
|
||||||
@@ -663,6 +684,7 @@
|
|||||||
[/^(\d+) hinzugefügt$/, '$1 added'],
|
[/^(\d+) hinzugefügt$/, '$1 added'],
|
||||||
[/^(\d+) bereits im Batch$/, '$1 already in the batch'],
|
[/^(\d+) bereits im Batch$/, '$1 already in the batch'],
|
||||||
[/^(\d+) ohne gültigen Account$/, '$1 without a valid account'],
|
[/^(\d+) ohne gültigen Account$/, '$1 without a valid account'],
|
||||||
|
[/^(\d+) von (\d+) Dateien werden hinzugefügt\. (\d+) durch den Dateinamenfilter ausgeschlossen\.$/, '$1 of $2 files will be added. $3 excluded by the filename filter.'],
|
||||||
[/^Sitzungsbericht mit 1 Upload exportiert$/, 'Session report with 1 upload exported'],
|
[/^Sitzungsbericht mit 1 Upload exportiert$/, 'Session report with 1 upload exported'],
|
||||||
[/^Sitzungsbericht mit (\d+) Uploads exportiert$/, 'Session report with $1 uploads exported'],
|
[/^Sitzungsbericht mit (\d+) Uploads exportiert$/, 'Session report with $1 uploads exported'],
|
||||||
[/^Login ok, Upload-Form bereit \(Dateifeld: (.+)\)$/, 'Login successful, upload form ready (file field: $1)'],
|
[/^Login ok, Upload-Form bereit \(Dateifeld: (.+)\)$/, 'Login successful, upload form ready (file field: $1)'],
|
||||||
@@ -782,6 +804,7 @@
|
|||||||
[/^(\d+) added$/, '$1 hinzugefügt'],
|
[/^(\d+) added$/, '$1 hinzugefügt'],
|
||||||
[/^(\d+) already in the batch$/, '$1 bereits im Batch'],
|
[/^(\d+) already in the batch$/, '$1 bereits im Batch'],
|
||||||
[/^(\d+) without a valid account$/, '$1 ohne gültigen Account'],
|
[/^(\d+) without a valid account$/, '$1 ohne gültigen Account'],
|
||||||
|
[/^(\d+) of (\d+) files will be added\. (\d+) excluded by the filename filter\.$/, '$1 von $2 Dateien werden hinzugefügt. $3 durch den Dateinamenfilter ausgeschlossen.'],
|
||||||
[/^Session report with 1 upload exported$/, 'Sitzungsbericht mit 1 Upload exportiert'],
|
[/^Session report with 1 upload exported$/, 'Sitzungsbericht mit 1 Upload exportiert'],
|
||||||
[/^Session report with (\d+) uploads exported$/, 'Sitzungsbericht mit $1 Uploads exportiert'],
|
[/^Session report with (\d+) uploads exported$/, 'Sitzungsbericht mit $1 Uploads exportiert'],
|
||||||
[/^Sleep in (\d+)s\.\.\.$/, 'Ruhezustand in $1s...'],
|
[/^Sleep in (\d+)s\.\.\.$/, 'Ruhezustand in $1s...'],
|
||||||
|
|||||||
+3
-2
@@ -209,10 +209,10 @@
|
|||||||
<div class="view-sidebar-summary view-sidebar-summary-block hoster-summary" id="hosterSummary">Keine Upload-Ziele ausgewählt</div>
|
<div class="view-sidebar-summary view-sidebar-summary-block hoster-summary" id="hosterSummary">Keine Upload-Ziele ausgewählt</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-telemetry" id="uploadTelemetry" aria-label="Upload-Statistik">
|
<div class="upload-telemetry" id="uploadTelemetry" aria-label="Upload-Statistik">
|
||||||
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Gesamt</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryTotal" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
|
||||||
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Verbindungen</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryConnections" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
|
||||||
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Verbleibend</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryRemaining" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Verbleibend</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryRemaining" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
||||||
|
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Gesamt</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryTotal" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
||||||
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Läuft</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryRunning" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Läuft</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryRunning" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
||||||
|
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Verbindungen</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryConnections" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
||||||
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Fertig</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryCompleted" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Fertig</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryCompleted" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
||||||
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Fehler</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryFailed" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Fehler</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryFailed" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
|
||||||
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Geschwindigkeit</span><strong class="upload-telemetry-value" id="uploadTelemetrySpeed" aria-label="0 B/s">0 B/s</strong></div>
|
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Geschwindigkeit</span><strong class="upload-telemetry-value" id="uploadTelemetrySpeed" aria-label="0 B/s">0 B/s</strong></div>
|
||||||
@@ -683,6 +683,7 @@
|
|||||||
<script src="../lib/serialized-runner.js"></script>
|
<script src="../lib/serialized-runner.js"></script>
|
||||||
<script src="../lib/speed-history.js"></script>
|
<script src="../lib/speed-history.js"></script>
|
||||||
<script src="../lib/upload-recovery.js"></script>
|
<script src="../lib/upload-recovery.js"></script>
|
||||||
|
<script src="../lib/filename-filter.js"></script>
|
||||||
<script src="account-submit.js"></script>
|
<script src="account-submit.js"></script>
|
||||||
<script src="account-status.js"></script>
|
<script src="account-status.js"></script>
|
||||||
<script src="history-status.js"></script>
|
<script src="history-status.js"></script>
|
||||||
|
|||||||
@@ -3303,6 +3303,103 @@ input[type="checkbox"] {
|
|||||||
border-color: var(--danger);
|
border-color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filename-filter-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-builder {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
transition: opacity 160ms ease, border-color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-builder.disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-policy {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-field label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-policy .hs-input,
|
||||||
|
.filename-filter-condition .hs-input,
|
||||||
|
.filename-filter-condition .key-input {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-conditions {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-condition {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(240px, 1.35fr) minmax(150px, 0.65fr) auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-rule-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-rule-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-footer .hint {
|
||||||
|
margin-left: auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.filename-filter-policy,
|
||||||
|
.filename-filter-condition {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-footer {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-filter-footer .hint {
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.settings-option-description,
|
.settings-option-description,
|
||||||
.hint {
|
.hint {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const sourceFiles = [
|
|||||||
'lib/doodstream-upload.js',
|
'lib/doodstream-upload.js',
|
||||||
'lib/file-probe.js',
|
'lib/file-probe.js',
|
||||||
'lib/file-discovery.js',
|
'lib/file-discovery.js',
|
||||||
|
'lib/filename-filter.js',
|
||||||
'lib/folder-monitor.js',
|
'lib/folder-monitor.js',
|
||||||
'lib/hosters.js',
|
'lib/hosters.js',
|
||||||
'lib/hoster-transport-error.js',
|
'lib/hoster-transport-error.js',
|
||||||
@@ -114,6 +115,7 @@ const sourceFiles = [
|
|||||||
'tests/doodstream-upload.test.js',
|
'tests/doodstream-upload.test.js',
|
||||||
'tests/file-probe.test.js',
|
'tests/file-probe.test.js',
|
||||||
'tests/file-discovery.test.js',
|
'tests/file-discovery.test.js',
|
||||||
|
'tests/filename-filter.test.js',
|
||||||
'tests/folder-monitor.test.js',
|
'tests/folder-monitor.test.js',
|
||||||
'tests/history-status.test.js',
|
'tests/history-status.test.js',
|
||||||
'tests/history-retention.test.js',
|
'tests/history-retention.test.js',
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ describe('ConfigStore', () => {
|
|||||||
assert.equal(config.globalSettings.scaleParallelUploads, false);
|
assert.equal(config.globalSettings.scaleParallelUploads, false);
|
||||||
assert.equal(config.globalSettings.lastBrowseDirectory, '');
|
assert.equal(config.globalSettings.lastBrowseDirectory, '');
|
||||||
assert.equal(config.globalSettings.pendingQueue, null);
|
assert.equal(config.globalSettings.pendingQueue, null);
|
||||||
|
assert.deepEqual(config.globalSettings.filenameFilter, {
|
||||||
|
enabled: false,
|
||||||
|
action: 'include',
|
||||||
|
matchMode: 'all',
|
||||||
|
conditions: []
|
||||||
|
});
|
||||||
assert.deepEqual(config.history, []);
|
assert.deepEqual(config.history, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const { describe, it } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const {
|
||||||
|
normalizeFilenameFilter,
|
||||||
|
evaluateFilenameFilter,
|
||||||
|
applyFilenameFilter
|
||||||
|
} = require('../lib/filename-filter');
|
||||||
|
|
||||||
|
describe('filename filter', () => {
|
||||||
|
it('accepts every file when the filter is disabled or has no usable conditions', () => {
|
||||||
|
const disabled = applyFilenameFilter(['Episode.1080p.mkv'], {
|
||||||
|
enabled: false,
|
||||||
|
action: 'exclude',
|
||||||
|
conditions: [{ operator: 'contains', value: '1080p' }]
|
||||||
|
});
|
||||||
|
const empty = applyFilenameFilter(['Episode.1080p.mkv'], {
|
||||||
|
enabled: true,
|
||||||
|
action: 'include',
|
||||||
|
conditions: [{ operator: 'contains', value: ' ' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(disabled.accepted, ['Episode.1080p.mkv']);
|
||||||
|
assert.deepEqual(disabled.excluded, []);
|
||||||
|
assert.equal(disabled.active, false);
|
||||||
|
assert.deepEqual(empty.accepted, ['Episode.1080p.mkv']);
|
||||||
|
assert.equal(empty.active, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes only filenames that satisfy every condition without case sensitivity', () => {
|
||||||
|
const filter = {
|
||||||
|
enabled: true,
|
||||||
|
action: 'include',
|
||||||
|
matchMode: 'all',
|
||||||
|
conditions: [
|
||||||
|
{ operator: 'contains', value: '720P' },
|
||||||
|
{ operator: 'notContains', value: 'sample' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyFilenameFilter([
|
||||||
|
{ path: 'C:/Shows/Episode.720p.mkv', name: 'Episode.720p.mkv' },
|
||||||
|
{ path: 'C:/Shows/Episode.720p.Sample.mkv', name: 'Episode.720p.Sample.mkv' },
|
||||||
|
{ path: 'C:/Shows/Episode.1080p.mkv', name: 'Episode.1080p.mkv' }
|
||||||
|
], filter);
|
||||||
|
|
||||||
|
assert.deepEqual(result.accepted.map(file => file.name), ['Episode.720p.mkv']);
|
||||||
|
assert.deepEqual(result.excluded.map(file => file.name), ['Episode.720p.Sample.mkv', 'Episode.1080p.mkv']);
|
||||||
|
assert.equal(result.total, 3);
|
||||||
|
assert.equal(result.active, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports matching any condition and excluding matching filenames', () => {
|
||||||
|
const filter = {
|
||||||
|
enabled: true,
|
||||||
|
action: 'exclude',
|
||||||
|
matchMode: 'any',
|
||||||
|
conditions: [
|
||||||
|
{ operator: 'contains', value: '1080p' },
|
||||||
|
{ operator: 'contains', value: 'sample' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(evaluateFilenameFilter('Episode.720p.mkv', filter).accepted, true);
|
||||||
|
assert.equal(evaluateFilenameFilter('Episode.1080p.mkv', filter).accepted, false);
|
||||||
|
assert.equal(evaluateFilenameFilter('Episode.720p.Sample.mkv', filter).accepted, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes unsupported values and derives names from paths', () => {
|
||||||
|
const normalized = normalizeFilenameFilter({
|
||||||
|
enabled: true,
|
||||||
|
action: 'unknown',
|
||||||
|
matchMode: 'unknown',
|
||||||
|
conditions: [
|
||||||
|
{ operator: 'unknown', value: ' 720p ' },
|
||||||
|
null,
|
||||||
|
{ operator: 'contains', value: '' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const result = applyFilenameFilter(['C:\\Shows\\Episode.720p.mkv', '/shows/Episode.1080p.mkv'], normalized);
|
||||||
|
|
||||||
|
assert.deepEqual(normalized, {
|
||||||
|
enabled: true,
|
||||||
|
action: 'include',
|
||||||
|
matchMode: 'all',
|
||||||
|
conditions: [{ operator: 'contains', value: '720p' }]
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.accepted, ['C:\\Shows\\Episode.720p.mkv']);
|
||||||
|
assert.deepEqual(result.excluded, ['/shows/Episode.1080p.mkv']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -74,6 +74,17 @@ test('translates duplicate desktop drop feedback to English', () => {
|
|||||||
assert.equal(translateText('Auswahl ist bereits in den Upload-Aufträgen.', 'en'), 'The selection is already in the upload jobs.');
|
assert.equal(translateText('Auswahl ist bereits in den Upload-Aufträgen.', 'en'), 'The selection is already in the upload jobs.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('translates filename filter controls and import counts in both directions', () => {
|
||||||
|
const german = '1 von 3 Dateien werden hinzugefügt. 2 durch den Dateinamenfilter ausgeschlossen.';
|
||||||
|
const english = '1 of 3 files will be added. 2 excluded by the filename filter.';
|
||||||
|
|
||||||
|
assert.equal(translateText('Dateinamen beim Hinzufügen filtern', 'en'), 'Filter filenames when adding files');
|
||||||
|
assert.equal(translateText('Dateiname filtern', 'en'), 'Filter file name');
|
||||||
|
assert.equal(translateText('enthält nicht', 'en'), 'does not contain');
|
||||||
|
assert.equal(translateText(german, 'en'), english);
|
||||||
|
assert.equal(translateText(english, 'de'), german);
|
||||||
|
});
|
||||||
|
|
||||||
test('rare account, backup, update, and confirmation states translate in both directions', () => {
|
test('rare account, backup, update, and confirmation states translate in both directions', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
['Einstellungen konnten vor dem Update nicht gespeichert werden', 'Settings could not be saved before the update'],
|
['Einstellungen konnten vor dem Update nicht gespeichert werden', 'Settings could not be saved before the update'],
|
||||||
|
|||||||
+21
-2
@@ -250,7 +250,7 @@ setTimeout(async () => {
|
|||||||
const englishSidebarHeadings = await wc.executeJavaScript('[...document.querySelectorAll("#upload-view, #accounts-view, #history-view")].map(view => [view.querySelector(".view-sidebar-kicker")?.textContent?.trim(), view.querySelector(".view-sidebar-title")?.textContent?.trim()].join("|"))');
|
const englishSidebarHeadings = await wc.executeJavaScript('[...document.querySelectorAll("#upload-view, #accounts-view, #history-view")].map(view => [view.querySelector(".view-sidebar-kicker")?.textContent?.trim(), view.querySelector(".view-sidebar-title")?.textContent?.trim()].join("|"))');
|
||||||
check('English sidebar hierarchy uses distinct translated kickers', englishSidebarHeadings.join('::') === 'Workspace|Uploads::Manage accounts|Accounts::Archive|History');
|
check('English sidebar hierarchy uses distinct translated kickers', englishSidebarHeadings.join('::') === 'Workspace|Uploads::Manage accounts|Accounts::Archive|History');
|
||||||
const englishTelemetryLabels = await wc.executeJavaScript('[...document.querySelectorAll("#uploadTelemetry .upload-telemetry-label")].map(el => el.textContent.trim()).join("|")');
|
const englishTelemetryLabels = await wc.executeJavaScript('[...document.querySelectorAll("#uploadTelemetry .upload-telemetry-label")].map(el => el.textContent.trim()).join("|")');
|
||||||
check('English upload telemetry is fully localized', englishTelemetryLabels === 'Total|Connections|Remaining|Running|Completed|Failed|Speed|ETA');
|
check('English upload telemetry is fully localized and ordered by relevance', englishTelemetryLabels === 'Remaining|Total|Running|Connections|Completed|Failed|Speed|ETA');
|
||||||
const englishLayoutFits = await wc.executeJavaScript('(() => { const states = [...document.querySelectorAll(".tab")].map(tab => { tab.click(); const view = document.querySelector(".view.active"); return view && view.scrollWidth <= view.clientWidth + 1; }); document.querySelector(".tab[data-view=upload]")?.click(); return states.every(Boolean) && document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1; })()');
|
const englishLayoutFits = await wc.executeJavaScript('(() => { const states = [...document.querySelectorAll(".tab")].map(tab => { tab.click(); const view = document.querySelector(".view.active"); return view && view.scrollWidth <= view.clientWidth + 1; }); document.querySelector(".tab[data-view=upload]")?.click(); return states.every(Boolean) && document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1; })()');
|
||||||
check('English labels fit every main view without horizontal overflow', englishLayoutFits === true);
|
check('English labels fit every main view without horizontal overflow', englishLayoutFits === true);
|
||||||
const speedSparklineAcrossTabs = await wc.executeJavaScript('(() => [...document.querySelectorAll(".tab")].map(tab => { tab.click(); const widget = document.getElementById("uploadSpeedSparkline"); const rect = widget?.getBoundingClientRect(); const style = widget && getComputedStyle(widget); return Boolean(widget && !widget.classList.contains("is-hidden") && style.visibility === "visible" && style.opacity === "1" && rect.width > 0 && rect.height > 0); }))()');
|
const speedSparklineAcrossTabs = await wc.executeJavaScript('(() => [...document.querySelectorAll(".tab")].map(tab => { tab.click(); const widget = document.getElementById("uploadSpeedSparkline"); const rect = widget?.getBoundingClientRect(); const style = widget && getComputedStyle(widget); return Boolean(widget && !widget.classList.contains("is-hidden") && style.visibility === "visible" && style.opacity === "1" && rect.width > 0 && rect.height > 0); }))()');
|
||||||
@@ -502,7 +502,7 @@ setTimeout(async () => {
|
|||||||
check('Recent panel labels are consistently German', localizedRecentTabs === 'Dateien|Statistik');
|
check('Recent panel labels are consistently German', localizedRecentTabs === 'Dateien|Statistik');
|
||||||
|
|
||||||
const localizedTelemetry = await wc.executeJavaScript('[...document.querySelectorAll("#uploadTelemetry .upload-telemetry-label")].map(el => el.textContent.trim()).join("|")');
|
const localizedTelemetry = await wc.executeJavaScript('[...document.querySelectorAll("#uploadTelemetry .upload-telemetry-label")].map(el => el.textContent.trim()).join("|")');
|
||||||
check('Upload telemetry exposes all eight German labels', localizedTelemetry === 'Gesamt|Verbindungen|Verbleibend|Läuft|Fertig|Fehler|Geschwindigkeit|ETA');
|
check('Upload telemetry exposes all eight German labels in the requested order', localizedTelemetry === 'Verbleibend|Gesamt|Läuft|Verbindungen|Fertig|Fehler|Geschwindigkeit|ETA');
|
||||||
|
|
||||||
const initialTelemetryValues = await wc.executeJavaScript('[...document.querySelectorAll("#uploadTelemetry .upload-telemetry-value")].map(el => el.getAttribute("aria-label") || el.textContent.trim()).join("|")');
|
const initialTelemetryValues = await wc.executeJavaScript('[...document.querySelectorAll("#uploadTelemetry .upload-telemetry-value")].map(el => el.getAttribute("aria-label") || el.textContent.trim()).join("|")');
|
||||||
check('Upload telemetry starts with stable empty values', initialTelemetryValues === '0|0|0|0|0|0|0 B/s|--:--');
|
check('Upload telemetry starts with stable empty values', initialTelemetryValues === '0|0|0|0|0|0|0 B/s|--:--');
|
||||||
@@ -1432,6 +1432,25 @@ setTimeout(async () => {
|
|||||||
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'uploads\\']")?.click()');
|
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'uploads\\']")?.click()');
|
||||||
const uploadSettingsState = await wc.executeJavaScript('(() => { const activePage = document.querySelector(".settings-subpage.active"); return [activePage?.dataset.subpage, activePage?.querySelector("h3")?.textContent.trim(), document.querySelector("label[for=removeFromQueueOnDoneInput]")?.textContent.trim(), document.getElementById("removeFromQueueOnDoneInput")?.closest(".settings-option")?.querySelector(".settings-option-description")?.textContent.trim()].join("|"); })()');
|
const uploadSettingsState = await wc.executeJavaScript('(() => { const activePage = document.querySelector(".settings-subpage.active"); return [activePage?.dataset.subpage, activePage?.querySelector("h3")?.textContent.trim(), document.querySelector("label[for=removeFromQueueOnDoneInput]")?.textContent.trim(), document.getElementById("removeFromQueueOnDoneInput")?.closest(".settings-option")?.querySelector(".settings-option-description")?.textContent.trim()].join("|"); })()');
|
||||||
check('Upload completion behavior is immediately findable', uploadSettingsState === 'uploads|Upload-Verhalten|Nach Abschluss aus der Liste entfernen|Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste.');
|
check('Upload completion behavior is immediately findable', uploadSettingsState === 'uploads|Upload-Verhalten|Nach Abschluss aus der Liste entfernen|Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste.');
|
||||||
|
const filenameFilterControls = await wc.executeJavaScript('(() => ({ api: typeof window.FilenameFilter?.applyFilenameFilter, enabled: document.getElementById("filenameFilterEnabledInput")?.checked, action: document.getElementById("filenameFilterActionInput")?.value, match: document.getElementById("filenameFilterMatchModeInput")?.value, rows: document.querySelectorAll("[data-filename-filter-condition]").length, add: Boolean(document.getElementById("addFilenameFilterConditionBtn")) }))()');
|
||||||
|
check('Filename filter starts disabled with a complete rule builder', filenameFilterControls.api === 'function' && filenameFilterControls.enabled === false && filenameFilterControls.action === 'include' && filenameFilterControls.match === 'all' && filenameFilterControls.rows === 1 && filenameFilterControls.add === true);
|
||||||
|
const filenameFilterGeometry = await wc.executeJavaScript('(() => { const rect = selector => { const r = document.querySelector(selector)?.getBoundingClientRect(); return r ? { left: r.left, right: r.right, top: r.top, bottom: r.bottom, width: r.width, height: r.height } : null; }; const valueLabel = rect("[data-filename-filter-value-label]"); const operatorLabel = rect("[data-filename-filter-operator-label]"); const value = rect("[data-filename-filter-value]"); const operator = rect("[data-filename-filter-operator]"); const remove = rect("[data-filename-filter-remove]"); const modeLabel = rect("label[for=filenameFilterMatchModeInput]"); const mode = rect("#filenameFilterMatchModeInput"); const actionLabel = rect("label[for=filenameFilterActionInput]"); const action = rect("#filenameFilterActionInput"); return { ruleLabelsAbove: valueLabel && operatorLabel && value && operator && valueLabel.bottom <= value.top - 4 && operatorLabel.bottom <= operator.top - 4, inputBeforeOperator: value && operator && value.left < operator.left, compactRule: value && operator && remove && operator.left - value.right >= 6 && operator.left - value.right <= 16 && remove.left - operator.right >= 6 && remove.left - operator.right <= 16, alignedRule: value && operator && remove && Math.abs(value.top - operator.top) <= 2 && Math.abs(operator.top - remove.top) <= 2 && Math.abs(value.height - operator.height) <= 2 && Math.abs(operator.height - remove.height) <= 2, policyBelowRule: value && mode && action && mode.top > value.bottom && action.top > value.bottom, policyOrder: mode && action && mode.left < action.left, policyLabelsAbove: modeLabel && mode && actionLabel && action && modeLabel.bottom <= mode.top - 4 && actionLabel.bottom <= action.top - 4, equalPolicyWidths: mode && action && Math.abs(mode.width - action.width) <= 2 }; })()');
|
||||||
|
check('Filename filter follows value, comparison, conditions, then action', filenameFilterGeometry.ruleLabelsAbove && filenameFilterGeometry.inputBeforeOperator && filenameFilterGeometry.compactRule && filenameFilterGeometry.alignedRule && filenameFilterGeometry.policyBelowRule && filenameFilterGeometry.policyOrder && filenameFilterGeometry.policyLabelsAbove && filenameFilterGeometry.equalPolicyWidths);
|
||||||
|
const filenameFilterPersistence = await wc.executeJavaScript('(async () => { const enabled = document.getElementById("filenameFilterEnabledInput"); enabled.checked = true; enabled.dispatchEvent(new Event("change", { bubbles: true })); const first = document.querySelector("[data-filename-filter-condition]"); first.querySelector("[data-filename-filter-operator]").value = "contains"; first.querySelector("[data-filename-filter-value]").value = "720p"; first.querySelector("[data-filename-filter-value]").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("addFilenameFilterConditionBtn").click(); const rows = [...document.querySelectorAll("[data-filename-filter-condition]")]; rows[1].querySelector("[data-filename-filter-operator]").value = "notContains"; rows[1].querySelector("[data-filename-filter-value]").value = "sample"; rows[1].querySelector("[data-filename-filter-value]").dispatchEvent(new Event("input", { bubbles: true })); const dirty = document.getElementById("saveSettingsBtn").disabled === false; await saveSettings({ feedbackText: "Gespeichert" }); const saved = (await window.api.getGlobalSettings()).filenameFilter; return { dirty, saved }; })()');
|
||||||
|
check('Filename filter conditions participate in dirty tracking and persist canonically', filenameFilterPersistence.dirty === true && filenameFilterPersistence.saved?.enabled === true && filenameFilterPersistence.saved?.action === 'include' && filenameFilterPersistence.saved?.matchMode === 'all' && JSON.stringify(filenameFilterPersistence.saved?.conditions) === JSON.stringify([{ operator: 'contains', value: '720p' }, { operator: 'notContains', value: 'sample' }]));
|
||||||
|
const filenameFilterImport = await wc.executeJavaScript('(() => { selectedFiles = []; _pendingFiles = []; queueJobs = []; rebuildJobIndex(); addPathsToQueue([{ path: "C:/filter/Episode.720p.mkv", name: "Episode.720p.mkv", size: 10 }, { path: "C:/filter/Episode.720p.Sample.mkv", name: "Episode.720p.Sample.mkv", size: 11 }, { path: "C:/filter/Episode.1080p.mkv", name: "Episode.1080p.mkv", size: 12 }]); return { modal: document.getElementById("hosterModal")?.style.display, description: document.getElementById("hosterModalDescription")?.textContent, pending: _pendingFiles.map(file => file.name) }; })()');
|
||||||
|
check('Filename filter previews accepted and excluded counts before host selection', filenameFilterImport.modal === 'flex' && filenameFilterImport.pending.length === 1 && filenameFilterImport.pending[0] === 'Episode.720p.mkv' && /1 von 3/.test(filenameFilterImport.description || '') && /2/.test(filenameFilterImport.description || ''));
|
||||||
|
const filenameFilterDropPaths = await wc.executeJavaScript('(async () => { cancelHosterModal(); await addDropTargetEntries([{ path: "C:/filter/Floating.720p.mkv" }, { path: "C:/filter/Floating.1080p.mkv" }]); const floating = _pendingFiles.map(file => file.name); cancelHosterModal(); await addDroppedFiles([{ path: "C:/filter/Desktop.720p.mkv", name: "Desktop.720p.mkv", size: 12, type: "video/x-matroska" }, { path: "C:/filter/Desktop.1080p.mkv", name: "Desktop.1080p.mkv", size: 12, type: "video/x-matroska" }]); const desktop = _pendingFiles.map(file => file.name); cancelHosterModal(); return { floating, desktop }; })()');
|
||||||
|
check('Filename filter applies identically to floating and native desktop drops', JSON.stringify(filenameFilterDropPaths.floating) === JSON.stringify(['Floating.720p.mkv']) && JSON.stringify(filenameFilterDropPaths.desktop) === JSON.stringify(['Desktop.720p.mkv']));
|
||||||
|
await wc.executeJavaScript('(() => { selectedFiles = []; _pendingFiles = []; queueJobs = []; rebuildJobIndex(); const toast = document.getElementById("copyToast"); toast.textContent = ""; toast.classList.remove("show"); config.globalSettings.folderMonitor = { ...(config.globalSettings.folderMonitor || {}), hosters: ["voe.sx"], autoStart: false }; })()');
|
||||||
|
wc.send('folder-monitor:new-files', ['C:/filter/Watched.720p.mkv', 'C:/filter/Watched.1080p.mkv']);
|
||||||
|
await waitUntil(() => wc.executeJavaScript('selectedFiles.length === 1'));
|
||||||
|
const filenameFilterFolderMonitor = await wc.executeJavaScript('(() => ({ files: selectedFiles.map(file => file.name), jobs: queueJobs.map(job => job.fileName), modal: document.getElementById("hosterModal")?.style.display, toast: document.getElementById("copyToast")?.textContent }))()');
|
||||||
|
check('Filename filter also applies to monitored folders with preset destinations', JSON.stringify(filenameFilterFolderMonitor.files) === JSON.stringify(['Watched.720p.mkv']) && filenameFilterFolderMonitor.jobs.every(name => name === 'Watched.720p.mkv') && filenameFilterFolderMonitor.modal !== 'flex' && /1 von 2/.test(filenameFilterFolderMonitor.toast || ''));
|
||||||
|
await wc.executeJavaScript('selectedFiles = []; queueJobs = []; rebuildJobIndex(); updateUploadView()');
|
||||||
|
const filenameFilterRejectAll = await wc.executeJavaScript('(() => { cancelHosterModal(); const toast = document.getElementById("copyToast"); toast.textContent = ""; toast.classList.remove("show"); const action = document.getElementById("filenameFilterActionInput"); action.value = "exclude"; action.dispatchEvent(new Event("change", { bubbles: true })); const rows = [...document.querySelectorAll("[data-filename-filter-condition]")]; rows[0].querySelector("[data-filename-filter-value]").value = "1080p"; rows[0].querySelector("[data-filename-filter-value]").dispatchEvent(new Event("input", { bubbles: true })); rows.slice(1).forEach(row => row.querySelector("[data-filename-filter-remove]")?.click()); config.globalSettings.filenameFilter = readFilenameFilterSettings(); addPathsToQueue([{ path: "C:/filter/Only.1080p.mkv", name: "Only.1080p.mkv", size: 10 }]); return { modal: document.getElementById("hosterModal")?.style.display, pending: _pendingFiles.length, toast: toast.textContent, shown: toast.classList.contains("show") }; })()');
|
||||||
|
check('A fully excluded import stays out of the queue and explains the result', filenameFilterRejectAll.modal !== 'flex' && filenameFilterRejectAll.pending === 0 && filenameFilterRejectAll.shown === true && /0 von 1/.test(filenameFilterRejectAll.toast || ''));
|
||||||
|
await wc.executeJavaScript('(() => { const enabled = document.getElementById("filenameFilterEnabledInput"); enabled.checked = false; enabled.dispatchEvent(new Event("change", { bubbles: true })); return saveSettings({ feedbackText: "Gespeichert" }); })()');
|
||||||
const plaintextCredentialOverride = await wc.executeJavaScript('(() => ({ control: document.getElementById("allowPlaintextCredentialStorageInput"), copy: document.body.textContent.includes("Unsichere Klartext-Speicherung"), bridge: typeof window.api.getSecretStoreStatus }))()');
|
const plaintextCredentialOverride = await wc.executeJavaScript('(() => ({ control: document.getElementById("allowPlaintextCredentialStorageInput"), copy: document.body.textContent.includes("Unsichere Klartext-Speicherung"), bridge: typeof window.api.getSecretStoreStatus }))()');
|
||||||
check('Settings expose no plaintext credential storage override', plaintextCredentialOverride.control === null && plaintextCredentialOverride.copy === false && plaintextCredentialOverride.bridge === 'undefined');
|
check('Settings expose no plaintext credential storage override', plaintextCredentialOverride.control === null && plaintextCredentialOverride.copy === false && plaintextCredentialOverride.bridge === 'undefined');
|
||||||
const settingsTypography = await wc.executeJavaScript('(() => { const size = selector => parseFloat(getComputedStyle(document.querySelector(selector)).fontSize); return { heading: size(".settings-subpage.active .settings-page-header h3"), intro: size(".settings-subpage.active .settings-page-header p"), section: size(".settings-subpage.active .settings-section-label"), rowLabel: size(".settings-subpage.active .settings-row > label"), hint: size(".settings-subpage.active .hint"), optionLabel: size(".settings-subpage.active .settings-option-copy label"), optionDescription: size(".settings-subpage.active .settings-option-description"), navigation: size(".settings-nav-button"), search: size("#settingsSearchInput") }; })()');
|
const settingsTypography = await wc.executeJavaScript('(() => { const size = selector => parseFloat(getComputedStyle(document.querySelector(selector)).fontSize); return { heading: size(".settings-subpage.active .settings-page-header h3"), intro: size(".settings-subpage.active .settings-page-header p"), section: size(".settings-subpage.active .settings-section-label"), rowLabel: size(".settings-subpage.active .settings-row > label"), hint: size(".settings-subpage.active .hint"), optionLabel: size(".settings-subpage.active .settings-option-copy label"), optionDescription: size(".settings-subpage.active .settings-option-description"), navigation: size(".settings-nav-button"), search: size("#settingsSearchInput") }; })()');
|
||||||
|
|||||||
Reference in New Issue
Block a user