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:
parent
c0b9ec9d17
commit
0851bb09fc
@ -32,6 +32,7 @@ const DEFAULTS = {
|
||||
parallelUploadCount: 0, // 0 = use per-hoster limits only
|
||||
scaleParallelUploads: false,
|
||||
removeFromQueueOnDone: false,
|
||||
showDropTarget: false,
|
||||
globalMaxSpeedKbs: 0, // 0 = unlimited global speed
|
||||
pendingQueue: null,
|
||||
scramble: {
|
||||
|
||||
62
main.js
62
main.js
@ -13,6 +13,7 @@ const backupCrypto = require('./lib/backup-crypto');
|
||||
const FolderMonitor = require('./lib/folder-monitor');
|
||||
|
||||
let mainWindow;
|
||||
let dropTargetWindow = null;
|
||||
let tray = null;
|
||||
const configStore = new ConfigStore(app);
|
||||
let uploadManager = null;
|
||||
@ -497,6 +498,14 @@ app.whenReady().then(() => {
|
||||
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
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
@ -518,6 +527,7 @@ app.on('window-all-closed', () => {
|
||||
|
||||
app.on('before-quit', () => {
|
||||
try { folderMonitor.stop(); } catch {}
|
||||
destroyDropTargetWindow();
|
||||
});
|
||||
|
||||
// --- IPC Handlers ---
|
||||
@ -893,6 +903,58 @@ ipcMain.handle('get-always-on-top', () => {
|
||||
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 ---
|
||||
let shutdownMode = 'nothing';
|
||||
let shutdownTimer = null;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "1.9.6",
|
||||
"version": "1.9.7",
|
||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
5
preload-drop-target.js
Normal file
5
preload-drop-target.js
Normal file
@ -0,0 +1,5 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('dropTargetApi', {
|
||||
sendFiles: (paths) => ipcRenderer.send('drop-target:files', paths)
|
||||
});
|
||||
@ -64,6 +64,13 @@ contextBridge.exposeInMainWorld('api', {
|
||||
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
|
||||
debugTestUpload: () => ipcRenderer.invoke('debug-test-upload'),
|
||||
debugLog: (msg) => ipcRenderer.invoke('debug-log', msg),
|
||||
@ -91,5 +98,6 @@ contextBridge.exposeInMainWorld('api', {
|
||||
ipcRenderer.removeAllListeners('app:update-progress');
|
||||
ipcRenderer.removeAllListeners('shutdown-countdown');
|
||||
ipcRenderer.removeAllListeners('folder-monitor:new-files');
|
||||
ipcRenderer.removeAllListeners('drop-target:files');
|
||||
}
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
// Restore always-on-top state
|
||||
@ -414,39 +419,10 @@ function setupDragDrop() {
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
// Allow drop on the entire 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('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('drag-over');
|
||||
_dropHandled = true;
|
||||
addDroppedFiles(e.dataTransfer.files).catch(console.error);
|
||||
});
|
||||
dropZone.addEventListener('click', () => pickFiles());
|
||||
@ -456,7 +432,6 @@ function setupDragDrop() {
|
||||
uploadView.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
if (e.target.closest('.drop-zone')) return; // handled above
|
||||
_dropHandled = true;
|
||||
addDroppedFiles(e.dataTransfer.files).catch(console.error);
|
||||
});
|
||||
}
|
||||
@ -1665,8 +1640,8 @@ function updateStatusBar() {
|
||||
const uploadedSize = Math.max(0, stats.totalSize - stats.remainingSize);
|
||||
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(stats.totalSize)}`;
|
||||
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
|
||||
document.getElementById('sbConnections').textContent = `Aktive Verbindungen: ${lastUploadStats.activeJobs || 0}`;
|
||||
document.getElementById('sbQueueCount').textContent = `Gesamt: ${stats.total}`;
|
||||
document.getElementById('sbConnections').textContent = `Connections: ${lastUploadStats.activeJobs || 0}`;
|
||||
document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`;
|
||||
document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`;
|
||||
document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`;
|
||||
document.getElementById('sbDoneCount').textContent = `Done: ${stats.done}`;
|
||||
@ -1780,6 +1755,10 @@ function renderSettings() {
|
||||
<label>Queue beim Start wiederherstellen</label>
|
||||
<input type="checkbox" class="settings-autosave" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}>
|
||||
</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 class="settings-section-label">Updates</div>
|
||||
<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)),
|
||||
scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.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)),
|
||||
folderMonitor: {
|
||||
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) {
|
||||
const hs = { ...(hosterSettings[name] || {}) };
|
||||
document.querySelectorAll(`.hs-input[data-hoster="${name}"]`).forEach(input => {
|
||||
|
||||
68
renderer/drop-target.html
Normal file
68
renderer/drop-target.html
Normal 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>
|
||||
@ -7,13 +7,6 @@
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<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">
|
||||
<button class="tab active" data-view="upload">Upload</button>
|
||||
<button class="tab" data-view="accounts">Accounts</button>
|
||||
@ -84,14 +77,14 @@
|
||||
<table class="queue-table" id="queueTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-filename sortable" data-sort="filename">Dateiname</th>
|
||||
<th class="col-size sortable" data-sort="size">Hochgeladen / Größe</th>
|
||||
<th class="col-filename sortable" data-sort="filename">Filename</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-status sortable" data-sort="status">Status</th>
|
||||
<th class="col-elapsed">Zeit</th>
|
||||
<th class="col-remaining">Rest</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>
|
||||
</thead>
|
||||
<tbody id="queueBody"></tbody>
|
||||
@ -118,7 +111,7 @@
|
||||
<thead id="recentFilesHead">
|
||||
<tr>
|
||||
<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-link sortable" data-recent-sort="link">Link<span class="sort-indicator">↕</span></th>
|
||||
</tr>
|
||||
|
||||
@ -169,36 +169,6 @@ body {
|
||||
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 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; }
|
||||
|
||||
/* Queue Container */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user