feat: add folder support and system tray icon
- Add "+ Ordner" button for recursive folder upload - Drag & drop auto-detects folders and resolves files recursively - Minimize to system tray instead of taskbar - Tray icon with context menu (Öffnen/Beenden) - Tray tooltip shows upload progress during active uploads - Fix folder detection heuristic (size === 0, not % 4096) - Fix concurrent drop guard to prevent double modal - Fix duplicate "Erneut versuchen" context menu entry - Add .catch() on async drop handlers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6b2b2ca04c
commit
0480da0437
70
main.js
70
main.js
@ -1,4 +1,4 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, clipboard, powerSaveBlocker, nativeTheme } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, clipboard, powerSaveBlocker, nativeTheme, Tray, Menu } = require('electron');
|
||||||
nativeTheme.themeSource = 'dark';
|
nativeTheme.themeSource = 'dark';
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@ -12,6 +12,7 @@ const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
|||||||
const backupCrypto = require('./lib/backup-crypto');
|
const backupCrypto = require('./lib/backup-crypto');
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
|
let tray = null;
|
||||||
const configStore = new ConfigStore(app);
|
const configStore = new ConfigStore(app);
|
||||||
let uploadManager = null;
|
let uploadManager = null;
|
||||||
const HEALTH_CHECK_TIMEOUT = 25000;
|
const HEALTH_CHECK_TIMEOUT = 25000;
|
||||||
@ -453,8 +454,35 @@ function createWindow() {
|
|||||||
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createTray() {
|
||||||
|
const iconPath = path.join(__dirname, 'assets', 'app_icon.ico');
|
||||||
|
tray = new Tray(iconPath);
|
||||||
|
tray.setToolTip('Multi-Hoster-Upload');
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{ label: 'Öffnen', click: () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Beenden', click: () => { app.quit(); } }
|
||||||
|
]);
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (mainWindow) { mainWindow.show(); mainWindow.focus(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTrayTooltip(text) {
|
||||||
|
if (tray && !tray.isDestroyed()) tray.setToolTip(text);
|
||||||
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
createTray();
|
||||||
|
|
||||||
|
// Minimize to tray instead of taskbar
|
||||||
|
mainWindow.on('minimize', () => {
|
||||||
|
mainWindow.hide();
|
||||||
|
});
|
||||||
|
|
||||||
// Auto-check for updates after 3 seconds
|
// Auto-check for updates after 3 seconds
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@ -536,9 +564,38 @@ ipcMain.handle('debug-test-upload', async () => {
|
|||||||
|
|
||||||
ipcMain.handle('select-folder', async () => {
|
ipcMain.handle('select-folder', async () => {
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
properties: ['openDirectory']
|
properties: ['openDirectory', 'multiSelections']
|
||||||
});
|
});
|
||||||
return result.canceled ? null : result.filePaths;
|
if (result.canceled || !result.filePaths.length) return null;
|
||||||
|
|
||||||
|
// Recursively collect all files from selected folders
|
||||||
|
const files = [];
|
||||||
|
const walk = (dir) => {
|
||||||
|
try {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) walk(full);
|
||||||
|
else if (entry.isFile()) files.push(full);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
for (const folder of result.filePaths) walk(folder);
|
||||||
|
return files.length > 0 ? files : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('resolve-folder-files', async (_event, folderPath) => {
|
||||||
|
const files = [];
|
||||||
|
const walk = (dir) => {
|
||||||
|
try {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) walk(full);
|
||||||
|
else if (entry.isFile()) files.push(full);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
walk(folderPath);
|
||||||
|
return files;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('start-upload', (_event, payload) => {
|
ipcMain.handle('start-upload', (_event, payload) => {
|
||||||
@ -583,6 +640,13 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('upload-stats', data);
|
mainWindow.webContents.send('upload-stats', data);
|
||||||
}
|
}
|
||||||
|
// Update tray tooltip with upload progress
|
||||||
|
if (data.state === 'uploading' && data.activeJobs > 0) {
|
||||||
|
const speedMb = ((data.globalSpeedKbs || 0) / 1024).toFixed(1);
|
||||||
|
updateTrayTooltip(`Upload: ${data.activeJobs} aktiv - ${speedMb} MB/s`);
|
||||||
|
} else {
|
||||||
|
updateTrayTooltip('Multi-Hoster-Upload');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadManager.on('batch-done', async (summary) => {
|
uploadManager.on('batch-done', async (summary) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "1.8.5",
|
"version": "1.8.6",
|
||||||
"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": {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// File selection
|
// File selection
|
||||||
selectFiles: () => ipcRenderer.invoke('select-files'),
|
selectFiles: () => ipcRenderer.invoke('select-files'),
|
||||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||||
|
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
||||||
|
|
||||||
// Upload control
|
// Upload control
|
||||||
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
||||||
|
|||||||
@ -391,7 +391,7 @@ function setupDragDrop() {
|
|||||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
|
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
|
||||||
dropZone.addEventListener('drop', (e) => {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('drag-over');
|
e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('drag-over');
|
||||||
addDroppedFiles(e.dataTransfer.files);
|
addDroppedFiles(e.dataTransfer.files).catch(console.error);
|
||||||
});
|
});
|
||||||
dropZone.addEventListener('click', () => pickFiles());
|
dropZone.addEventListener('click', () => pickFiles());
|
||||||
|
|
||||||
@ -400,38 +400,77 @@ function setupDragDrop() {
|
|||||||
uploadView.addEventListener('drop', (e) => {
|
uploadView.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.target.closest('.drop-zone')) return; // handled above
|
if (e.target.closest('.drop-zone')) return; // handled above
|
||||||
addDroppedFiles(e.dataTransfer.files);
|
addDroppedFiles(e.dataTransfer.files).catch(console.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let _pendingFiles = []; // Files waiting for hoster modal confirmation
|
let _pendingFiles = []; // Files waiting for hoster modal confirmation
|
||||||
|
|
||||||
function addDroppedFiles(fileList) {
|
let _addingDropped = false;
|
||||||
const files = Array.from(fileList);
|
|
||||||
const existingPaths = new Set([
|
async function addDroppedFiles(fileList) {
|
||||||
...selectedFiles.map(f => f.path),
|
if (_addingDropped) return;
|
||||||
..._pendingFiles.map(f => f.path)
|
_addingDropped = true;
|
||||||
]);
|
try {
|
||||||
const newFiles = [];
|
const files = Array.from(fileList);
|
||||||
for (const file of files) {
|
const existingPaths = new Set([
|
||||||
// Use webUtils.getPathForFile (Electron 33+) with fallback to file.path
|
...selectedFiles.map(f => f.path),
|
||||||
let filePath = '';
|
..._pendingFiles.map(f => f.path)
|
||||||
try { filePath = window.api.getPathForFile(file); } catch { filePath = file.path || ''; }
|
]);
|
||||||
const fileName = file.name || '';
|
const newFiles = [];
|
||||||
if (filePath && !existingPaths.has(filePath)) {
|
|
||||||
newFiles.push({ path: filePath, name: fileName, size: file.size });
|
for (const file of files) {
|
||||||
existingPaths.add(filePath);
|
let filePath = '';
|
||||||
|
try { filePath = window.api.getPathForFile(file); } catch { filePath = file.path || ''; }
|
||||||
|
if (!filePath) continue;
|
||||||
|
|
||||||
|
// Detect folders: directories report size 0 and empty type in Electron drag-and-drop
|
||||||
|
if (file.type === '' && file.size === 0) {
|
||||||
|
try {
|
||||||
|
const folderFiles = await window.api.resolveFolderFiles(filePath);
|
||||||
|
if (folderFiles && folderFiles.length > 0) {
|
||||||
|
for (const fp of folderFiles) {
|
||||||
|
if (!existingPaths.has(fp)) {
|
||||||
|
const name = fp.split('\\').pop().split('/').pop();
|
||||||
|
newFiles.push({ path: fp, name, size: null });
|
||||||
|
existingPaths.add(fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular file
|
||||||
|
const fileName = file.name || '';
|
||||||
|
if (!existingPaths.has(filePath)) {
|
||||||
|
newFiles.push({ path: filePath, name: fileName, size: file.size });
|
||||||
|
existingPaths.add(filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (newFiles.length > 0) {
|
if (newFiles.length > 0) {
|
||||||
_pendingFiles.push(...newFiles);
|
_pendingFiles.push(...newFiles);
|
||||||
openHosterModal();
|
openHosterModal();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_addingDropped = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickFiles() {
|
async function pickFiles() {
|
||||||
const paths = await window.api.selectFiles();
|
const paths = await window.api.selectFiles();
|
||||||
if (!paths) return;
|
if (!paths) return;
|
||||||
|
addPathsToQueue(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickFolder() {
|
||||||
|
const paths = await window.api.selectFolder();
|
||||||
|
if (!paths) return;
|
||||||
|
addPathsToQueue(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPathsToQueue(paths) {
|
||||||
const newFiles = [];
|
const newFiles = [];
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
if (!selectedFiles.find(f => f.path === p) && !_pendingFiles.find(f => f.path === p)) {
|
if (!selectedFiles.find(f => f.path === p) && !_pendingFiles.find(f => f.path === p)) {
|
||||||
@ -2327,6 +2366,7 @@ window.addEventListener('beforeunload', () => {
|
|||||||
// --- Setup Listeners ---
|
// --- Setup Listeners ---
|
||||||
function setupListeners() {
|
function setupListeners() {
|
||||||
document.getElementById('addFilesBtn').addEventListener('click', pickFiles);
|
document.getElementById('addFilesBtn').addEventListener('click', pickFiles);
|
||||||
|
document.getElementById('addFolderBtn').addEventListener('click', pickFolder);
|
||||||
document.getElementById('startUploadBtn').addEventListener('click', startUpload);
|
document.getElementById('startUploadBtn').addEventListener('click', startUpload);
|
||||||
document.getElementById('startSelectedBtn').addEventListener('click', startSelectedUpload);
|
document.getElementById('startSelectedBtn').addEventListener('click', startSelectedUpload);
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<button class="btn btn-xs btn-primary" id="addFilesBtn">+ Dateien</button>
|
<button class="btn btn-xs btn-primary" id="addFilesBtn">+ Dateien</button>
|
||||||
|
<button class="btn btn-xs btn-secondary" id="addFolderBtn">+ Ordner</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -272,8 +273,6 @@
|
|||||||
<div class="ctx-separator"></div>
|
<div class="ctx-separator"></div>
|
||||||
<div class="ctx-item" data-action="delete-selected">Entfernen</div>
|
<div class="ctx-item" data-action="delete-selected">Entfernen</div>
|
||||||
<div class="ctx-item" data-action="delete-all">Alle entfernen</div>
|
<div class="ctx-item" data-action="delete-all">Alle entfernen</div>
|
||||||
<div class="ctx-separator"></div>
|
|
||||||
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="context-menu" id="recentContextMenu" style="display:none">
|
<div class="context-menu" id="recentContextMenu" style="display:none">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user