feat: add folder monitoring (Ordnerüberwachung) and fix statusbar display
- New FolderMonitor class with chokidar for watching folders - Settings UI panel with all options (extensions filter, recursive, auto-start, skip duplicates) - Auto-queue and auto-upload when files appear in monitored folder - Fix statusbar to show uploaded/remaining instead of cumulative session bytes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0480da0437
commit
b5841c69f5
@ -40,6 +40,16 @@ const DEFAULTS = {
|
|||||||
suffix: '',
|
suffix: '',
|
||||||
chars: 'both', // 'letters' | 'numbers' | 'both'
|
chars: 'both', // 'letters' | 'numbers' | 'both'
|
||||||
length: 0 // 0 = same as original basename length
|
length: 0 // 0 = same as original basename length
|
||||||
|
},
|
||||||
|
folderMonitor: {
|
||||||
|
enabled: false,
|
||||||
|
folderPath: '',
|
||||||
|
recursive: false,
|
||||||
|
filterMode: 'include', // 'include' | 'exclude'
|
||||||
|
extensions: '', // comma-separated: 'mp4,mkv,avi'
|
||||||
|
skipDuplicates: true,
|
||||||
|
delaySec: 3,
|
||||||
|
autoStart: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
history: []
|
history: []
|
||||||
@ -116,10 +126,18 @@ class ConfigStore {
|
|||||||
...(data.hosterSettings && data.hosterSettings[name] || {})
|
...(data.hosterSettings && data.hosterSettings[name] || {})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const savedGlobal = data.globalSettings || {};
|
||||||
const globalSettings = {
|
const globalSettings = {
|
||||||
...DEFAULTS.globalSettings,
|
...DEFAULTS.globalSettings,
|
||||||
...(data.globalSettings || {})
|
...savedGlobal
|
||||||
};
|
};
|
||||||
|
// Deep-merge nested objects so new keys are always present
|
||||||
|
for (const key of Object.keys(DEFAULTS.globalSettings)) {
|
||||||
|
const def = DEFAULTS.globalSettings[key];
|
||||||
|
if (def && typeof def === 'object' && !Array.isArray(def)) {
|
||||||
|
globalSettings[key] = { ...def, ...(savedGlobal[key] || {}) };
|
||||||
|
}
|
||||||
|
}
|
||||||
return { hosters, hosterSettings, globalSettings, history: data.history || [] };
|
return { hosters, hosterSettings, globalSettings, history: data.history || [] };
|
||||||
} catch {
|
} catch {
|
||||||
return JSON.parse(JSON.stringify(DEFAULTS));
|
return JSON.parse(JSON.stringify(DEFAULTS));
|
||||||
|
|||||||
98
lib/folder-monitor.js
Normal file
98
lib/folder-monitor.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const path = require('path');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
|
class FolderMonitor extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._watcher = null;
|
||||||
|
this._settings = null;
|
||||||
|
this._seenFiles = new Set();
|
||||||
|
this._batchBuffer = [];
|
||||||
|
this._batchTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get running() {
|
||||||
|
return !!this._watcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(settings) {
|
||||||
|
this.stop();
|
||||||
|
this._settings = settings;
|
||||||
|
|
||||||
|
const folderPath = String(settings.folderPath || '').trim();
|
||||||
|
if (!folderPath) throw new Error('Kein Ordnerpfad angegeben');
|
||||||
|
|
||||||
|
const watchOptions = {
|
||||||
|
persistent: true,
|
||||||
|
ignoreInitial: true,
|
||||||
|
depth: settings.recursive ? undefined : 0,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
stabilityThreshold: Math.max(1000, (settings.delaySec || 3) * 1000),
|
||||||
|
pollInterval: 500
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._watcher = chokidar.watch(folderPath, watchOptions);
|
||||||
|
this._watcher.on('add', (filePath) => this._onNewFile(filePath));
|
||||||
|
this._watcher.on('error', (err) => this.emit('error', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this._watcher) {
|
||||||
|
this._watcher.close().catch(() => {});
|
||||||
|
this._watcher = null;
|
||||||
|
}
|
||||||
|
if (this._batchTimer) {
|
||||||
|
clearTimeout(this._batchTimer);
|
||||||
|
this._batchTimer = null;
|
||||||
|
}
|
||||||
|
this._batchBuffer = [];
|
||||||
|
this._seenFiles = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
return {
|
||||||
|
running: this.running,
|
||||||
|
folderPath: this._settings ? this._settings.folderPath : '',
|
||||||
|
seenCount: this._seenFiles.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onNewFile(filePath) {
|
||||||
|
const settings = this._settings;
|
||||||
|
if (!settings) return;
|
||||||
|
|
||||||
|
// Extension filter
|
||||||
|
const ext = path.extname(filePath).replace(/^\./, '').toLowerCase();
|
||||||
|
const rawExtensions = String(settings.extensions || '').trim();
|
||||||
|
if (rawExtensions) {
|
||||||
|
const extList = rawExtensions.split(',').map(e => e.trim().toLowerCase().replace(/^\./, '')).filter(Boolean);
|
||||||
|
if (extList.length > 0) {
|
||||||
|
const matches = extList.includes(ext);
|
||||||
|
if (settings.filterMode === 'include' && !matches) return;
|
||||||
|
if (settings.filterMode === 'exclude' && matches) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip duplicates (session-based)
|
||||||
|
if (settings.skipDuplicates) {
|
||||||
|
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
||||||
|
if (this._seenFiles.has(normalized)) return;
|
||||||
|
this._seenFiles.add(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch: collect files over 200ms window then emit together
|
||||||
|
this._batchBuffer.push(filePath);
|
||||||
|
if (this._batchTimer) clearTimeout(this._batchTimer);
|
||||||
|
this._batchTimer = setTimeout(() => {
|
||||||
|
const files = this._batchBuffer.splice(0);
|
||||||
|
this._batchTimer = null;
|
||||||
|
if (files.length > 0) {
|
||||||
|
this.emit('new-files', files);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FolderMonitor;
|
||||||
62
main.js
62
main.js
@ -10,11 +10,13 @@ const VoeUploader = require('./lib/voe-upload');
|
|||||||
const DoodstreamUploader = require('./lib/doodstream-upload');
|
const DoodstreamUploader = require('./lib/doodstream-upload');
|
||||||
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
||||||
const backupCrypto = require('./lib/backup-crypto');
|
const backupCrypto = require('./lib/backup-crypto');
|
||||||
|
const FolderMonitor = require('./lib/folder-monitor');
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let tray = null;
|
let tray = null;
|
||||||
const configStore = new ConfigStore(app);
|
const configStore = new ConfigStore(app);
|
||||||
let uploadManager = null;
|
let uploadManager = null;
|
||||||
|
let folderMonitor = new FolderMonitor();
|
||||||
const HEALTH_CHECK_TIMEOUT = 25000;
|
const HEALTH_CHECK_TIMEOUT = 25000;
|
||||||
|
|
||||||
// --- Debug logging (writes to upload-debug.log next to the app) ---
|
// --- Debug logging (writes to upload-debug.log next to the app) ---
|
||||||
@ -484,6 +486,17 @@ app.whenReady().then(() => {
|
|||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-start folder monitor if enabled
|
||||||
|
try {
|
||||||
|
const launchConfig = configStore.load();
|
||||||
|
const fm = launchConfig.globalSettings && launchConfig.globalSettings.folderMonitor;
|
||||||
|
if (fm && fm.enabled && fm.folderPath) {
|
||||||
|
startFolderMonitor(fm);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
debugLog(`folder-monitor auto-start failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-check for updates after 3 seconds
|
// Auto-check for updates after 3 seconds
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
@ -499,6 +512,10 @@ app.on('window-all-closed', () => {
|
|||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
try { folderMonitor.stop(); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
// --- IPC Handlers ---
|
// --- IPC Handlers ---
|
||||||
|
|
||||||
// Debug log from renderer
|
// Debug log from renderer
|
||||||
@ -811,6 +828,51 @@ ipcMain.handle('save-global-settings', async (_event, globalSettings) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Folder Monitor ---
|
||||||
|
function startFolderMonitor(settings) {
|
||||||
|
try {
|
||||||
|
folderMonitor.stop();
|
||||||
|
folderMonitor.removeAllListeners();
|
||||||
|
folderMonitor.on('new-files', (files) => {
|
||||||
|
debugLog(`folder-monitor: ${files.length} new file(s)`);
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('folder-monitor:new-files', files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
folderMonitor.on('error', (err) => {
|
||||||
|
debugLog(`folder-monitor error: ${err.message}`);
|
||||||
|
});
|
||||||
|
folderMonitor.start(settings);
|
||||||
|
debugLog(`folder-monitor started: ${settings.folderPath}`);
|
||||||
|
} catch (err) {
|
||||||
|
debugLog(`folder-monitor start failed: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('folder-monitor:start', (_event, settings) => {
|
||||||
|
startFolderMonitor(settings);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('folder-monitor:stop', () => {
|
||||||
|
folderMonitor.stop();
|
||||||
|
debugLog('folder-monitor stopped');
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('folder-monitor:status', () => {
|
||||||
|
return folderMonitor.status();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('folder-monitor:select-folder', async () => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ['openDirectory']
|
||||||
|
});
|
||||||
|
if (result.canceled || !result.filePaths.length) return null;
|
||||||
|
return result.filePaths[0];
|
||||||
|
});
|
||||||
|
|
||||||
// --- Always on top ---
|
// --- Always on top ---
|
||||||
ipcMain.handle('set-always-on-top', async (_event, value) => {
|
ipcMain.handle('set-always-on-top', async (_event, value) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
|||||||
184
package-lock.json
generated
184
package-lock.json
generated
@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "1.0.0",
|
"version": "1.8.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "1.0.0",
|
"version": "1.8.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chokidar": "^3.6.0",
|
||||||
"undici": "^7.16.0"
|
"undici": "^7.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -901,6 +902,19 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/anymatch": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"picomatch": "^2.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/app-builder-bin": {
|
"node_modules/app-builder-bin": {
|
||||||
"version": "5.0.0-alpha.10",
|
"version": "5.0.0-alpha.10",
|
||||||
"resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz",
|
"resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz",
|
||||||
@ -1202,6 +1216,18 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/binary-extensions": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
@ -1253,6 +1279,18 @@
|
|||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/braces": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fill-range": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
@ -1523,6 +1561,30 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||||
@ -2684,6 +2746,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fill-range": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"to-regex-range": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@ -2774,6 +2848,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@ -2892,6 +2980,18 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob/node_modules/balanced-match": {
|
"node_modules/glob/node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -3273,6 +3373,18 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-binary-path": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"binary-extensions": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-ci": {
|
"node_modules/is-ci": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
|
||||||
@ -3302,6 +3414,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@ -3312,6 +3433,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-glob": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-interactive": {
|
"node_modules/is-interactive": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||||
@ -3329,6 +3462,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-number": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-unicode-supported": {
|
"node_modules/is-unicode-supported": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||||
@ -4058,9 +4200,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -4288,6 +4428,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/plist": {
|
"node_modules/plist": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
|
||||||
@ -4461,6 +4613,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@ -5047,6 +5211,18 @@
|
|||||||
"tmp": "^0.2.0"
|
"tmp": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/to-regex-range": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-number": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/truncate-utf8-bytes": {
|
"node_modules/truncate-utf8-bytes": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"release:gitea": "node scripts/release_gitea.mjs"
|
"release:gitea": "node scripts/release_gitea.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chokidar": "^3.6.0",
|
||||||
"undici": "^7.16.0"
|
"undici": "^7.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
10
preload.js
10
preload.js
@ -55,6 +55,15 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
exportBackup: (password) => ipcRenderer.invoke('export-backup', password),
|
exportBackup: (password) => ipcRenderer.invoke('export-backup', password),
|
||||||
importBackup: (password) => ipcRenderer.invoke('import-backup', password),
|
importBackup: (password) => ipcRenderer.invoke('import-backup', password),
|
||||||
|
|
||||||
|
// Folder Monitor
|
||||||
|
folderMonitorStart: (settings) => ipcRenderer.invoke('folder-monitor:start', settings),
|
||||||
|
folderMonitorStop: () => ipcRenderer.invoke('folder-monitor:stop'),
|
||||||
|
folderMonitorStatus: () => ipcRenderer.invoke('folder-monitor:status'),
|
||||||
|
folderMonitorSelectFolder: () => ipcRenderer.invoke('folder-monitor:select-folder'),
|
||||||
|
onFolderMonitorNewFiles: (callback) => {
|
||||||
|
ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data));
|
||||||
|
},
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
debugTestUpload: () => ipcRenderer.invoke('debug-test-upload'),
|
debugTestUpload: () => ipcRenderer.invoke('debug-test-upload'),
|
||||||
debugLog: (msg) => ipcRenderer.invoke('debug-log', msg),
|
debugLog: (msg) => ipcRenderer.invoke('debug-log', msg),
|
||||||
@ -81,5 +90,6 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
ipcRenderer.removeAllListeners('app:update-available');
|
ipcRenderer.removeAllListeners('app:update-available');
|
||||||
ipcRenderer.removeAllListeners('app:update-progress');
|
ipcRenderer.removeAllListeners('app:update-progress');
|
||||||
ipcRenderer.removeAllListeners('shutdown-countdown');
|
ipcRenderer.removeAllListeners('shutdown-countdown');
|
||||||
|
ipcRenderer.removeAllListeners('folder-monitor:new-files');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
123
renderer/app.js
123
renderer/app.js
@ -76,6 +76,31 @@ async function init() {
|
|||||||
});
|
});
|
||||||
window.api.onShutdownCountdown(handleShutdownCountdown);
|
window.api.onShutdownCountdown(handleShutdownCountdown);
|
||||||
|
|
||||||
|
// Folder monitor: auto-queue new files
|
||||||
|
window.api.onFolderMonitorNewFiles((files) => {
|
||||||
|
window.api.debugLog('folder-monitor: received ' + files.length + ' file(s)');
|
||||||
|
const fm = config.globalSettings && config.globalSettings.folderMonitor;
|
||||||
|
const hosters = getSelectedHosters();
|
||||||
|
if (hosters.length > 0 && fm && fm.autoStart) {
|
||||||
|
// Add files directly to queue and start upload
|
||||||
|
const newFiles = [];
|
||||||
|
for (const p of files) {
|
||||||
|
if (!selectedFiles.find(f => f.path === p) && !_pendingFiles.find(f => f.path === p)) {
|
||||||
|
const name = p.split('\\').pop().split('/').pop();
|
||||||
|
newFiles.push({ path: p, name, size: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newFiles.length > 0) {
|
||||||
|
selectedFiles.push(...newFiles);
|
||||||
|
buildQueuePreview();
|
||||||
|
updateUploadView();
|
||||||
|
if (!uploading && !healthCheckRunning) startUpload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addPathsToQueue(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.api.debugLog('init complete, all listeners registered');
|
window.api.debugLog('init complete, all listeners registered');
|
||||||
|
|
||||||
// Restore always-on-top state
|
// Restore always-on-top state
|
||||||
@ -1605,7 +1630,8 @@ function updateStatusBar() {
|
|||||||
|
|
||||||
document.getElementById('sbState').textContent = stateText;
|
document.getElementById('sbState').textContent = stateText;
|
||||||
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
|
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
|
||||||
document.getElementById('sbTotal').textContent = `${formatSize(lastUploadStats.totalBytes || 0)} / ${formatSize(stats.totalSize)}`;
|
const uploadedSize = Math.max(0, stats.totalSize - stats.remainingSize);
|
||||||
|
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(stats.remainingSize)}`;
|
||||||
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
|
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
|
||||||
document.getElementById('sbConnections').textContent = `Aktive Verbindungen ${lastUploadStats.activeJobs || 0}`;
|
document.getElementById('sbConnections').textContent = `Aktive Verbindungen ${lastUploadStats.activeJobs || 0}`;
|
||||||
document.getElementById('sbQueueCount').textContent = `Gesamt ${stats.total}`;
|
document.getElementById('sbQueueCount').textContent = `Gesamt ${stats.total}`;
|
||||||
@ -1738,6 +1764,74 @@ function renderSettings() {
|
|||||||
`;
|
`;
|
||||||
container.appendChild(generalPanel);
|
container.appendChild(generalPanel);
|
||||||
|
|
||||||
|
// --- Folder Monitor Panel ---
|
||||||
|
const fm = globalSettings.folderMonitor || {};
|
||||||
|
const folderMonitorPanel = document.createElement('div');
|
||||||
|
folderMonitorPanel.className = 'hoster-settings-panel';
|
||||||
|
folderMonitorPanel.innerHTML = `
|
||||||
|
<div class="hoster-panel-header" data-hoster="folderMonitor">
|
||||||
|
<span class="panel-arrow">▶</span>
|
||||||
|
<span class="panel-title">Ordnerüberwachung</span>
|
||||||
|
<span class="panel-status" id="folderMonitorStatusBadge">${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hoster-panel-body" data-panel="folderMonitor" style="display:none">
|
||||||
|
<div class="settings-grid-mini">
|
||||||
|
<div class="settings-row checkbox-row">
|
||||||
|
<label>Aktiviert</label>
|
||||||
|
<input type="checkbox" class="settings-autosave" id="fmEnabledInput" ${fm.enabled ? 'checked' : ''}>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<label>Ordnerpfad</label>
|
||||||
|
<input type="text" class="key-input settings-autosave" id="fmFolderPathInput" value="${escapeAttr(fm.folderPath || '')}" placeholder="Ordner wählen...">
|
||||||
|
<button class="btn btn-xs btn-secondary" id="fmChooseFolderBtn">Wählen</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row checkbox-row">
|
||||||
|
<label>Unterordner einbeziehen</label>
|
||||||
|
<input type="checkbox" class="settings-autosave" id="fmRecursiveInput" ${fm.recursive ? 'checked' : ''}>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<label>Dateierweiterungen</label>
|
||||||
|
<select class="hs-input settings-autosave" id="fmFilterModeInput" style="width:auto;margin-right:6px">
|
||||||
|
<option value="include" ${fm.filterMode === 'include' ? 'selected' : ''}>Nur diese</option>
|
||||||
|
<option value="exclude" ${fm.filterMode === 'exclude' ? 'selected' : ''}>Alle außer</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" class="key-input settings-autosave" id="fmExtensionsInput" value="${escapeAttr(fm.extensions || '')}" placeholder="mp4,mkv,avi" style="flex:1">
|
||||||
|
</div>
|
||||||
|
<div class="settings-row checkbox-row">
|
||||||
|
<label>Duplikate überspringen</label>
|
||||||
|
<input type="checkbox" class="settings-autosave" id="fmSkipDuplicatesInput" ${fm.skipDuplicates !== false ? 'checked' : ''}>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<label>Verzögerung (Sekunden)</label>
|
||||||
|
<input type="number" class="hs-input settings-autosave" id="fmDelaySecInput" value="${fm.delaySec ?? 3}" min="1" max="300">
|
||||||
|
<span class="hint">Warten bis Datei fertig geschrieben</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row checkbox-row">
|
||||||
|
<label>Auto-Upload starten</label>
|
||||||
|
<input type="checkbox" class="settings-autosave" id="fmAutoStartInput" ${fm.autoStart !== false ? 'checked' : ''}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(folderMonitorPanel);
|
||||||
|
|
||||||
|
// Toggle folder monitor panel
|
||||||
|
folderMonitorPanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
|
||||||
|
const body = folderMonitorPanel.querySelector('.hoster-panel-body');
|
||||||
|
const arrow = folderMonitorPanel.querySelector('.panel-arrow');
|
||||||
|
const isOpen = body.style.display !== 'none';
|
||||||
|
body.style.display = isOpen ? 'none' : 'block';
|
||||||
|
arrow.innerHTML = isOpen ? '▶' : '▼';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('fmChooseFolderBtn')?.addEventListener('click', async () => {
|
||||||
|
const folder = await window.api.folderMonitorSelectFolder();
|
||||||
|
if (folder) {
|
||||||
|
document.getElementById('fmFolderPathInput').value = folder;
|
||||||
|
scheduleSettingsSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (configuredAccounts.length === 0) {
|
if (configuredAccounts.length === 0) {
|
||||||
const empty = document.createElement('div');
|
const empty = document.createElement('div');
|
||||||
empty.className = 'settings-empty';
|
empty.className = 'settings-empty';
|
||||||
@ -1841,7 +1935,17 @@ async function saveSettings(options = {}) {
|
|||||||
parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
|
parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
|
||||||
scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
|
scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
|
||||||
removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked,
|
removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked,
|
||||||
globalMaxSpeedKbs: Math.max(0, Math.round((parseFloat(document.getElementById('globalMaxSpeedMbsInput')?.value || '0') || 0) * 1024))
|
globalMaxSpeedKbs: Math.max(0, Math.round((parseFloat(document.getElementById('globalMaxSpeedMbsInput')?.value || '0') || 0) * 1024)),
|
||||||
|
folderMonitor: {
|
||||||
|
enabled: !!document.getElementById('fmEnabledInput')?.checked,
|
||||||
|
folderPath: (document.getElementById('fmFolderPathInput')?.value || '').trim(),
|
||||||
|
recursive: !!document.getElementById('fmRecursiveInput')?.checked,
|
||||||
|
filterMode: document.getElementById('fmFilterModeInput')?.value || 'include',
|
||||||
|
extensions: (document.getElementById('fmExtensionsInput')?.value || '').trim(),
|
||||||
|
skipDuplicates: !!document.getElementById('fmSkipDuplicatesInput')?.checked,
|
||||||
|
delaySec: Math.max(1, parseInt(document.getElementById('fmDelaySecInput')?.value || '3', 10) || 3),
|
||||||
|
autoStart: !!document.getElementById('fmAutoStartInput')?.checked
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always on top setting
|
// Always on top setting
|
||||||
@ -1870,6 +1974,21 @@ async function saveSettings(options = {}) {
|
|||||||
hosterSettings = config.hosterSettings || {};
|
hosterSettings = config.hosterSettings || {};
|
||||||
clearTimeout(settingsSaveTimer);
|
clearTimeout(settingsSaveTimer);
|
||||||
|
|
||||||
|
// Start/stop folder monitor based on settings
|
||||||
|
const fmSettings = globalSettings.folderMonitor;
|
||||||
|
const badge = document.getElementById('folderMonitorStatusBadge');
|
||||||
|
if (fmSettings && fmSettings.enabled && fmSettings.folderPath) {
|
||||||
|
try {
|
||||||
|
await window.api.folderMonitorStart(fmSettings);
|
||||||
|
if (badge) { badge.textContent = 'Aktiv'; badge.className = 'panel-status active'; }
|
||||||
|
} catch {
|
||||||
|
if (badge) { badge.textContent = 'Fehler'; badge.className = 'panel-status'; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await window.api.folderMonitorStop();
|
||||||
|
if (badge) { badge.textContent = 'Inaktiv'; badge.className = 'panel-status'; }
|
||||||
|
}
|
||||||
|
|
||||||
const feedback = document.getElementById('saveFeedback');
|
const feedback = document.getElementById('saveFeedback');
|
||||||
feedback.textContent = feedbackText;
|
feedback.textContent = feedbackText;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user