feat: floating drop target window and English column labels

- Small always-on-top drop target window (toggle in Settings > Allgemein)
- Files dropped on it get added to the queue with hoster modal
- Auto-shows on app start if previously enabled
- Column headers now in English (Filename, Uploaded/Size, Progress)
- Statusbar labels in English (Connections, Total)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-12 04:11:01 +01:00
parent c0b9ec9d17
commit 0851bb09fc
9 changed files with 168 additions and 74 deletions

View File

@ -32,6 +32,7 @@ const DEFAULTS = {
parallelUploadCount: 0, // 0 = use per-hoster limits only parallelUploadCount: 0, // 0 = use per-hoster limits only
scaleParallelUploads: false, scaleParallelUploads: false,
removeFromQueueOnDone: false, removeFromQueueOnDone: false,
showDropTarget: false,
globalMaxSpeedKbs: 0, // 0 = unlimited global speed globalMaxSpeedKbs: 0, // 0 = unlimited global speed
pendingQueue: null, pendingQueue: null,
scramble: { scramble: {

62
main.js
View File

@ -13,6 +13,7 @@ const backupCrypto = require('./lib/backup-crypto');
const FolderMonitor = require('./lib/folder-monitor'); const FolderMonitor = require('./lib/folder-monitor');
let mainWindow; let mainWindow;
let dropTargetWindow = null;
let tray = null; let tray = null;
const configStore = new ConfigStore(app); const configStore = new ConfigStore(app);
let uploadManager = null; let uploadManager = null;
@ -497,6 +498,14 @@ app.whenReady().then(() => {
debugLog(`folder-monitor auto-start failed: ${err.message}`); debugLog(`folder-monitor auto-start failed: ${err.message}`);
} }
// Auto-show drop target if enabled
try {
const dtConfig = configStore.load();
if (dtConfig.globalSettings && dtConfig.globalSettings.showDropTarget) {
createDropTargetWindow();
}
} catch {}
// Auto-check for updates after 3 seconds // Auto-check for updates after 3 seconds
setTimeout(async () => { setTimeout(async () => {
try { try {
@ -518,6 +527,7 @@ app.on('window-all-closed', () => {
app.on('before-quit', () => { app.on('before-quit', () => {
try { folderMonitor.stop(); } catch {} try { folderMonitor.stop(); } catch {}
destroyDropTargetWindow();
}); });
// --- IPC Handlers --- // --- IPC Handlers ---
@ -893,6 +903,58 @@ ipcMain.handle('get-always-on-top', () => {
return false; return false;
}); });
// --- Drop Target Window ---
function createDropTargetWindow() {
if (dropTargetWindow && !dropTargetWindow.isDestroyed()) return;
const { screen } = require('electron');
const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize;
dropTargetWindow = new BrowserWindow({
width: 120,
height: 120,
x: width - 140,
y: height - 140,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
minimizable: false,
maximizable: false,
focusable: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'preload-drop-target.js')
}
});
dropTargetWindow.loadFile('renderer/drop-target.html');
dropTargetWindow.on('closed', () => { dropTargetWindow = null; });
}
function destroyDropTargetWindow() {
if (dropTargetWindow && !dropTargetWindow.isDestroyed()) {
dropTargetWindow.close();
dropTargetWindow = null;
}
}
ipcMain.handle('show-drop-target', () => {
createDropTargetWindow();
return true;
});
ipcMain.handle('hide-drop-target', () => {
destroyDropTargetWindow();
return true;
});
ipcMain.on('drop-target:files', (_event, paths) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('drop-target:files', paths);
}
});
// --- Shutdown after finish --- // --- Shutdown after finish ---
let shutdownMode = 'nothing'; let shutdownMode = 'nothing';
let shutdownTimer = null; let shutdownTimer = null;

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "1.9.6", "version": "1.9.7",
"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": {

5
preload-drop-target.js Normal file
View File

@ -0,0 +1,5 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('dropTargetApi', {
sendFiles: (paths) => ipcRenderer.send('drop-target:files', paths)
});

View File

@ -64,6 +64,13 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data)); ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data));
}, },
// Drop Target
showDropTarget: () => ipcRenderer.invoke('show-drop-target'),
hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'),
onDropTargetFiles: (callback) => {
ipcRenderer.on('drop-target:files', (_event, paths) => callback(paths));
},
// 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),
@ -91,5 +98,6 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.removeAllListeners('app:update-progress'); ipcRenderer.removeAllListeners('app:update-progress');
ipcRenderer.removeAllListeners('shutdown-countdown'); ipcRenderer.removeAllListeners('shutdown-countdown');
ipcRenderer.removeAllListeners('folder-monitor:new-files'); ipcRenderer.removeAllListeners('folder-monitor:new-files');
ipcRenderer.removeAllListeners('drop-target:files');
} }
}); });

View File

@ -104,6 +104,11 @@ async function init() {
} }
}); });
// Drop target window: files dropped on the small floating window
window.api.onDropTargetFiles((paths) => {
addPathsToQueue(paths);
});
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
@ -414,39 +419,10 @@ function setupDragDrop() {
const dropZone = document.getElementById('dropZone'); const dropZone = document.getElementById('dropZone');
// Allow drop on the entire upload view // Allow drop on the entire upload view
const uploadView = document.getElementById('upload-view'); const uploadView = document.getElementById('upload-view');
const dropOverlay = document.getElementById('dropOverlay');
// Window-level drag overlay
let dragCounter = 0;
window.addEventListener('dragenter', (e) => {
e.preventDefault();
if (!e.dataTransfer.types.includes('Files')) return;
dragCounter++;
dropOverlay.classList.add('visible');
});
window.addEventListener('dragleave', (e) => {
e.preventDefault();
dragCounter--;
if (dragCounter <= 0) { dragCounter = 0; dropOverlay.classList.remove('visible'); }
});
window.addEventListener('dragover', (e) => { e.preventDefault(); });
let _dropHandled = false;
window.addEventListener('drop', (e) => {
e.preventDefault();
dragCounter = 0;
dropOverlay.classList.remove('visible');
// Process files dropped anywhere (fallback if no specific handler caught it)
if (!_dropHandled && e.dataTransfer.files.length > 0) {
addDroppedFiles(e.dataTransfer.files).catch(console.error);
}
_dropHandled = false;
});
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('drag-over'); });
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');
_dropHandled = true;
addDroppedFiles(e.dataTransfer.files).catch(console.error); addDroppedFiles(e.dataTransfer.files).catch(console.error);
}); });
dropZone.addEventListener('click', () => pickFiles()); dropZone.addEventListener('click', () => pickFiles());
@ -456,7 +432,6 @@ 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
_dropHandled = true;
addDroppedFiles(e.dataTransfer.files).catch(console.error); addDroppedFiles(e.dataTransfer.files).catch(console.error);
}); });
} }
@ -1665,8 +1640,8 @@ function updateStatusBar() {
const uploadedSize = Math.max(0, stats.totalSize - stats.remainingSize); const uploadedSize = Math.max(0, stats.totalSize - stats.remainingSize);
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(stats.totalSize)}`; document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(stats.totalSize)}`;
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 = `Connections: ${lastUploadStats.activeJobs || 0}`;
document.getElementById('sbQueueCount').textContent = `Gesamt: ${stats.total}`; document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`;
document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`; document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`;
document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`; document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`;
document.getElementById('sbDoneCount').textContent = `Done: ${stats.done}`; document.getElementById('sbDoneCount').textContent = `Done: ${stats.done}`;
@ -1780,6 +1755,10 @@ function renderSettings() {
<label>Queue beim Start wiederherstellen</label> <label>Queue beim Start wiederherstellen</label>
<input type="checkbox" class="settings-autosave" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}> <input type="checkbox" class="settings-autosave" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}>
</div> </div>
<div class="settings-row checkbox-row">
<label>Drop-Target anzeigen</label>
<input type="checkbox" class="settings-autosave" id="showDropTargetInput" ${globalSettings.showDropTarget ? 'checked' : ''}>
</div>
</div> </div>
<div class="settings-section-label">Updates</div> <div class="settings-section-label">Updates</div>
<div class="settings-row"> <div class="settings-row">
@ -2056,6 +2035,7 @@ 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,
showDropTarget: !!document.getElementById('showDropTargetInput')?.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: { folderMonitor: {
enabled: !!document.getElementById('fmEnabledInput')?.checked, enabled: !!document.getElementById('fmEnabledInput')?.checked,
@ -2080,6 +2060,13 @@ async function saveSettings(options = {}) {
} }
} }
// Drop target window
const dtCheckbox = document.getElementById('showDropTargetInput');
if (dtCheckbox) {
if (dtCheckbox.checked) await window.api.showDropTarget();
else await window.api.hideDropTarget();
}
for (const name of HOSTERS) { for (const name of HOSTERS) {
const hs = { ...(hosterSettings[name] || {}) }; const hs = { ...(hosterSettings[name] || {}) };
document.querySelectorAll(`.hs-input[data-hoster="${name}"]`).forEach(input => { document.querySelectorAll(`.hs-input[data-hoster="${name}"]`).forEach(input => {

68
renderer/drop-target.html Normal file
View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: transparent;
-webkit-app-region: drag;
user-select: none;
}
.target {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed rgba(126, 220, 255, 0.5);
border-radius: 10px;
background: rgba(22, 24, 28, 0.85);
transition: border-color 0.15s, background 0.15s;
}
.target.drag-over {
border-color: rgba(126, 220, 255, 0.9);
background: rgba(62, 167, 255, 0.15);
}
.icon {
font-size: 64px;
font-weight: 200;
color: rgba(126, 220, 255, 0.7);
line-height: 1;
-webkit-app-region: no-drag;
}
</style>
</head>
<body>
<div class="target" id="target">
<div class="icon">+</div>
</div>
<script>
const target = document.getElementById('target');
target.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
target.classList.add('drag-over');
});
target.addEventListener('dragleave', (e) => {
e.preventDefault();
target.classList.remove('drag-over');
});
target.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
target.classList.remove('drag-over');
const paths = [];
for (const file of e.dataTransfer.files) {
if (file.path) paths.push(file.path);
}
if (paths.length > 0) {
window.dropTargetApi.sendFiles(paths);
}
});
</script>
</body>
</html>

View File

@ -7,13 +7,6 @@
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<div id="dropOverlay" class="drop-overlay">
<div class="drop-overlay-content">
<div class="drop-overlay-icon">+</div>
<p>Dateien hier ablegen</p>
</div>
</div>
<nav class="tab-bar"> <nav class="tab-bar">
<button class="tab active" data-view="upload">Upload</button> <button class="tab active" data-view="upload">Upload</button>
<button class="tab" data-view="accounts">Accounts</button> <button class="tab" data-view="accounts">Accounts</button>
@ -84,14 +77,14 @@
<table class="queue-table" id="queueTable"> <table class="queue-table" id="queueTable">
<thead> <thead>
<tr> <tr>
<th class="col-filename sortable" data-sort="filename">Dateiname</th> <th class="col-filename sortable" data-sort="filename">Filename</th>
<th class="col-size sortable" data-sort="size">Hochgeladen / Größe</th> <th class="col-size sortable" data-sort="size">Uploaded / Size</th>
<th class="col-host sortable" data-sort="host">Host</th> <th class="col-host sortable" data-sort="host">Host</th>
<th class="col-status sortable" data-sort="status">Status</th> <th class="col-status sortable" data-sort="status">Status</th>
<th class="col-elapsed">Zeit</th> <th class="col-elapsed">Zeit</th>
<th class="col-remaining">Rest</th> <th class="col-remaining">Rest</th>
<th class="col-speed sortable" data-sort="speed">Speed</th> <th class="col-speed sortable" data-sort="speed">Speed</th>
<th class="col-progress sortable" data-sort="progress">Fortschritt</th> <th class="col-progress sortable" data-sort="progress">Progress</th>
</tr> </tr>
</thead> </thead>
<tbody id="queueBody"></tbody> <tbody id="queueBody"></tbody>
@ -118,7 +111,7 @@
<thead id="recentFilesHead"> <thead id="recentFilesHead">
<tr> <tr>
<th class="col-date sortable" data-recent-sort="date">Datum<span class="sort-indicator"></span></th> <th class="col-date sortable" data-recent-sort="date">Datum<span class="sort-indicator"></span></th>
<th class="col-filename sortable" data-recent-sort="filename">Dateiname<span class="sort-indicator"></span></th> <th class="col-filename sortable" data-recent-sort="filename">Filename<span class="sort-indicator"></span></th>
<th class="col-host sortable" data-recent-sort="host">Host<span class="sort-indicator"></span></th> <th class="col-host sortable" data-recent-sort="host">Host<span class="sort-indicator"></span></th>
<th class="col-link sortable" data-recent-sort="link">Link<span class="sort-indicator"></span></th> <th class="col-link sortable" data-recent-sort="link">Link<span class="sort-indicator"></span></th>
</tr> </tr>

View File

@ -169,36 +169,6 @@ body {
box-shadow: var(--panel-shadow); box-shadow: var(--panel-shadow);
} }
.drop-zone:hover, .drop-zone.drag-over { border-color: rgba(126, 220, 255, 0.6); background-color: rgba(62, 167, 255, 0.06); } .drop-zone:hover, .drop-zone.drag-over { border-color: rgba(126, 220, 255, 0.6); background-color: rgba(62, 167, 255, 0.06); }
/* Drop overlay (full-window drop target) */
.drop-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(16, 18, 22, 0.92);
backdrop-filter: blur(6px);
align-items: center;
justify-content: center;
pointer-events: none;
}
.drop-overlay.visible { display: flex; }
.drop-overlay-content {
text-align: center;
color: rgba(126, 220, 255, 0.9);
}
.drop-overlay-icon {
font-size: 120px;
font-weight: 200;
line-height: 1;
margin-bottom: 8px;
color: rgba(126, 220, 255, 0.8);
}
.drop-overlay-content p {
font-size: 16px;
color: var(--text-muted);
margin: 0;
}
.drop-icon { font-size: 40px; margin-bottom: 8px; } .drop-icon { font-size: 40px; margin-bottom: 8px; }
/* Queue Container */ /* Queue Container */