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';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@ -12,6 +12,7 @@ const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
||||
const backupCrypto = require('./lib/backup-crypto');
|
||||
|
||||
let mainWindow;
|
||||
let tray = null;
|
||||
const configStore = new ConfigStore(app);
|
||||
let uploadManager = null;
|
||||
const HEALTH_CHECK_TIMEOUT = 25000;
|
||||
@ -453,8 +454,35 @@ function createWindow() {
|
||||
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(() => {
|
||||
createWindow();
|
||||
createTray();
|
||||
|
||||
// Minimize to tray instead of taskbar
|
||||
mainWindow.on('minimize', () => {
|
||||
mainWindow.hide();
|
||||
});
|
||||
|
||||
// Auto-check for updates after 3 seconds
|
||||
setTimeout(async () => {
|
||||
@ -536,9 +564,38 @@ ipcMain.handle('debug-test-upload', async () => {
|
||||
|
||||
ipcMain.handle('select-folder', async () => {
|
||||
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) => {
|
||||
@ -583,6 +640,13 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
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) => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "1.8.5",
|
||||
"version": "1.8.6",
|
||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@ -27,6 +27,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
// File selection
|
||||
selectFiles: () => ipcRenderer.invoke('select-files'),
|
||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
||||
|
||||
// Upload control
|
||||
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('drop', (e) => {
|
||||
e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('drag-over');
|
||||
addDroppedFiles(e.dataTransfer.files);
|
||||
addDroppedFiles(e.dataTransfer.files).catch(console.error);
|
||||
});
|
||||
dropZone.addEventListener('click', () => pickFiles());
|
||||
|
||||
@ -400,38 +400,77 @@ function setupDragDrop() {
|
||||
uploadView.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
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
|
||||
|
||||
function addDroppedFiles(fileList) {
|
||||
const files = Array.from(fileList);
|
||||
const existingPaths = new Set([
|
||||
...selectedFiles.map(f => f.path),
|
||||
..._pendingFiles.map(f => f.path)
|
||||
]);
|
||||
const newFiles = [];
|
||||
for (const file of files) {
|
||||
// Use webUtils.getPathForFile (Electron 33+) with fallback to file.path
|
||||
let filePath = '';
|
||||
try { filePath = window.api.getPathForFile(file); } catch { filePath = file.path || ''; }
|
||||
const fileName = file.name || '';
|
||||
if (filePath && !existingPaths.has(filePath)) {
|
||||
newFiles.push({ path: filePath, name: fileName, size: file.size });
|
||||
existingPaths.add(filePath);
|
||||
let _addingDropped = false;
|
||||
|
||||
async function addDroppedFiles(fileList) {
|
||||
if (_addingDropped) return;
|
||||
_addingDropped = true;
|
||||
try {
|
||||
const files = Array.from(fileList);
|
||||
const existingPaths = new Set([
|
||||
...selectedFiles.map(f => f.path),
|
||||
..._pendingFiles.map(f => f.path)
|
||||
]);
|
||||
const newFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
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) {
|
||||
_pendingFiles.push(...newFiles);
|
||||
openHosterModal();
|
||||
|
||||
if (newFiles.length > 0) {
|
||||
_pendingFiles.push(...newFiles);
|
||||
openHosterModal();
|
||||
}
|
||||
} finally {
|
||||
_addingDropped = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pickFiles() {
|
||||
const paths = await window.api.selectFiles();
|
||||
if (!paths) return;
|
||||
addPathsToQueue(paths);
|
||||
}
|
||||
|
||||
async function pickFolder() {
|
||||
const paths = await window.api.selectFolder();
|
||||
if (!paths) return;
|
||||
addPathsToQueue(paths);
|
||||
}
|
||||
|
||||
function addPathsToQueue(paths) {
|
||||
const newFiles = [];
|
||||
for (const p of paths) {
|
||||
if (!selectedFiles.find(f => f.path === p) && !_pendingFiles.find(f => f.path === p)) {
|
||||
@ -2327,6 +2366,7 @@ window.addEventListener('beforeunload', () => {
|
||||
// --- Setup Listeners ---
|
||||
function setupListeners() {
|
||||
document.getElementById('addFilesBtn').addEventListener('click', pickFiles);
|
||||
document.getElementById('addFolderBtn').addEventListener('click', pickFolder);
|
||||
document.getElementById('startUploadBtn').addEventListener('click', startUpload);
|
||||
document.getElementById('startSelectedBtn').addEventListener('click', startSelectedUpload);
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn btn-xs btn-primary" id="addFilesBtn">+ Dateien</button>
|
||||
<button class="btn btn-xs btn-secondary" id="addFolderBtn">+ Ordner</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -272,8 +273,6 @@
|
||||
<div class="ctx-separator"></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-separator"></div>
|
||||
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div>
|
||||
</div>
|
||||
|
||||
<div class="context-menu" id="recentContextMenu" style="display:none">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user