Compare commits
No commits in common. "9bbeffb2df40a27a91727629906b4f65bce34a34" and "6d6453dc4be4d1ece35128b07ac728fae2390d0f" have entirely different histories.
9bbeffb2df
...
6d6453dc4b
1
.gitignore
vendored
1
.gitignore
vendored
@ -37,4 +37,3 @@ deploy/forgejo/caddy/data/
|
|||||||
deploy/forgejo/caddy/config/
|
deploy/forgejo/caddy/config/
|
||||||
deploy/forgejo/caddy/logs/
|
deploy/forgejo/caddy/logs/
|
||||||
deploy/forgejo/backups/
|
deploy/forgejo/backups/
|
||||||
.secrets
|
|
||||||
|
|||||||
13
CLAUDE.md
13
CLAUDE.md
@ -6,15 +6,10 @@
|
|||||||
|
|
||||||
## Releasen
|
## Releasen
|
||||||
|
|
||||||
Der Token liegt in `.secrets` (gitignored) und wird automatisch geladen.
|
1. Token setzen:
|
||||||
|
- PowerShell: `$env:GITEA_TOKEN="<token>"`
|
||||||
Als KI-Agent: Token aus `.secrets` lesen und als Umgebungsvariable setzen, dann Release-Script ausführen:
|
2. Release ausführen:
|
||||||
```bash
|
- `npm run release:gitea -- <version> [notes]`
|
||||||
export $(cat .secrets | xargs) && npm run release:gitea -- <version> [notes]
|
|
||||||
```
|
|
||||||
|
|
||||||
Manuell in PowerShell (falls nötig):
|
|
||||||
- `npm run release:gitea -- <version> [notes]` (Token ist bereits als Benutzervariable gesetzt)
|
|
||||||
|
|
||||||
Das Script:
|
Das Script:
|
||||||
- bumped `package.json`
|
- bumped `package.json`
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.6.92",
|
"version": "1.6.91",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -102,7 +102,6 @@ export function defaultSettings(): AppSettings {
|
|||||||
extractCpuPriority: "high",
|
extractCpuPriority: "high",
|
||||||
autoExtractWhenStopped: true,
|
autoExtractWhenStopped: true,
|
||||||
disabledProviders: [],
|
disabledProviders: [],
|
||||||
hosterRouting: {},
|
hosterRouting: {}
|
||||||
scheduledStartEpochMs: 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,6 @@ let mainWindow: BrowserWindow | null = null;
|
|||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
let clipboardTimer: ReturnType<typeof setInterval> | null = null;
|
let clipboardTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
let updateQuitTimer: ReturnType<typeof setTimeout> | null = null;
|
let updateQuitTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let scheduledStartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let lastClipboardText = "";
|
let lastClipboardText = "";
|
||||||
const controller = new AppController();
|
const controller = new AppController();
|
||||||
const CLIPBOARD_MAX_TEXT_CHARS = 50_000;
|
const CLIPBOARD_MAX_TEXT_CHARS = 50_000;
|
||||||
@ -267,26 +266,6 @@ function registerIpcHandlers(): void {
|
|||||||
const result = controller.updateSettings(validated as Partial<AppSettings>);
|
const result = controller.updateSettings(validated as Partial<AppSettings>);
|
||||||
updateClipboardWatcher();
|
updateClipboardWatcher();
|
||||||
updateTray();
|
updateTray();
|
||||||
// Manage scheduled-start timer
|
|
||||||
if (scheduledStartTimer !== null) {
|
|
||||||
clearTimeout(scheduledStartTimer);
|
|
||||||
scheduledStartTimer = null;
|
|
||||||
}
|
|
||||||
const schedMs = result.scheduledStartEpochMs || 0;
|
|
||||||
if (schedMs > 0) {
|
|
||||||
const delay = schedMs - Date.now();
|
|
||||||
if (delay <= 0) {
|
|
||||||
// Time already passed — start immediately and clear setting
|
|
||||||
void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`));
|
|
||||||
controller.updateSettings({ scheduledStartEpochMs: 0 });
|
|
||||||
} else {
|
|
||||||
scheduledStartTimer = setTimeout(() => {
|
|
||||||
scheduledStartTimer = null;
|
|
||||||
void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`));
|
|
||||||
controller.updateSettings({ scheduledStartEpochMs: 0 });
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => {
|
ipcMain.handle(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => {
|
||||||
|
|||||||
@ -1017,9 +1017,6 @@ export function App(): ReactElement {
|
|||||||
const [scheduleSpeedInputs, setScheduleSpeedInputs] = useState<Record<string, string>>({});
|
const [scheduleSpeedInputs, setScheduleSpeedInputs] = useState<Record<string, string>>({});
|
||||||
const [accountColumnWidths, setAccountColumnWidths] = useState<Record<AccountColumnKey, number>>(loadAccountColumnWidths);
|
const [accountColumnWidths, setAccountColumnWidths] = useState<Record<AccountColumnKey, number>>(loadAccountColumnWidths);
|
||||||
const [settingsDirty, setSettingsDirty] = useState(false);
|
const [settingsDirty, setSettingsDirty] = useState(false);
|
||||||
const [schedulePickerOpen, setSchedulePickerOpen] = useState(false);
|
|
||||||
const [scheduleTimeInput, setScheduleTimeInput] = useState("");
|
|
||||||
const [scheduleCountdown, setScheduleCountdown] = useState("");
|
|
||||||
const settingsDirtyRef = useRef(false);
|
const settingsDirtyRef = useRef(false);
|
||||||
const settingsDraftRevisionRef = useRef(0);
|
const settingsDraftRevisionRef = useRef(0);
|
||||||
const panelDirtyRevisionRef = useRef(0);
|
const panelDirtyRevisionRef = useRef(0);
|
||||||
@ -1202,23 +1199,6 @@ export function App(): ReactElement {
|
|||||||
setSpeedLimitInput(formatMbpsInputFromKbps(settingsDraft.speedLimitKbps));
|
setSpeedLimitInput(formatMbpsInputFromKbps(settingsDraft.speedLimitKbps));
|
||||||
}, [settingsDraft.speedLimitKbps]);
|
}, [settingsDraft.speedLimitKbps]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const schedMs = snapshot.settings.scheduledStartEpochMs || 0;
|
|
||||||
if (schedMs <= 0) { setScheduleCountdown(""); return; }
|
|
||||||
const update = (): void => {
|
|
||||||
const remaining = schedMs - Date.now();
|
|
||||||
if (remaining <= 0) { setScheduleCountdown(""); return; }
|
|
||||||
const totalSec = Math.ceil(remaining / 1000);
|
|
||||||
const h = Math.floor(totalSec / 3600);
|
|
||||||
const m = Math.floor((totalSec % 3600) / 60);
|
|
||||||
const s = totalSec % 60;
|
|
||||||
setScheduleCountdown(h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${s}s` : `${s}s`);
|
|
||||||
};
|
|
||||||
update();
|
|
||||||
const timer = setInterval(update, 1000);
|
|
||||||
return () => clearInterval(timer);
|
|
||||||
}, [snapshot.settings.scheduledStartEpochMs]);
|
|
||||||
|
|
||||||
const showToast = useCallback((message: string, timeoutMs = 2200): void => {
|
const showToast = useCallback((message: string, timeoutMs = 2200): void => {
|
||||||
setStatusToast(message);
|
setStatusToast(message);
|
||||||
if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); }
|
if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); }
|
||||||
@ -1584,10 +1564,6 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
// Setzt providerOrder + backwards-kompatible Felder synchron
|
// Setzt providerOrder + backwards-kompatible Felder synchron
|
||||||
const setProviderOrder = useCallback((newOrder: DebridProvider[]) => {
|
const setProviderOrder = useCallback((newOrder: DebridProvider[]) => {
|
||||||
settingsDraftRevisionRef.current += 1;
|
|
||||||
panelDirtyRevisionRef.current += 1;
|
|
||||||
settingsDirtyRef.current = true;
|
|
||||||
setSettingsDirty(true);
|
|
||||||
setSettingsDraft((prev) => ({
|
setSettingsDraft((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
providerOrder: newOrder,
|
providerOrder: newOrder,
|
||||||
@ -3211,44 +3187,6 @@ export function App(): ReactElement {
|
|||||||
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="4" y="4" width="16" height="16" rx="2" fill="currentColor" /></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="4" y="4" width="16" height="16" rx="2" fill="currentColor" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<div className="ctrl-separator" />
|
<div className="ctrl-separator" />
|
||||||
<div className="schedule-ctrl">
|
|
||||||
{(snapshot.settings.scheduledStartEpochMs || 0) > 0 ? (
|
|
||||||
<div className="schedule-active">
|
|
||||||
<span className="schedule-badge" title="Geplanter Start">⏰ {scheduleCountdown || new Date(snapshot.settings.scheduledStartEpochMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</span>
|
|
||||||
<button className="schedule-cancel" title="Geplanten Start abbrechen" onClick={() => { void window.rd.updateSettings({ scheduledStartEpochMs: 0 }).catch(() => {}); }}>✕</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={`ctrl-icon-btn schedule-btn${schedulePickerOpen ? " active" : ""}`}
|
|
||||||
title="Download-Start planen"
|
|
||||||
onClick={() => { setSchedulePickerOpen((v) => !v); setScheduleTimeInput(""); }}
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="9"/><polyline points="12,6 12,12 16,14"/></svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{schedulePickerOpen && (snapshot.settings.scheduledStartEpochMs || 0) === 0 && (
|
|
||||||
<div className="schedule-picker">
|
|
||||||
<span className="schedule-picker-label">Starten um</span>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
className="schedule-time-input"
|
|
||||||
value={scheduleTimeInput}
|
|
||||||
onChange={(e) => setScheduleTimeInput(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button className="btn btn-sm btn-primary" onClick={() => {
|
|
||||||
if (!scheduleTimeInput) return;
|
|
||||||
const [hStr, mStr] = scheduleTimeInput.split(":");
|
|
||||||
const now = new Date();
|
|
||||||
const target = new Date(now);
|
|
||||||
target.setHours(Number(hStr), Number(mStr), 0, 0);
|
|
||||||
if (target.getTime() <= now.getTime()) target.setDate(target.getDate() + 1);
|
|
||||||
void window.rd.updateSettings({ scheduledStartEpochMs: target.getTime() }).catch(() => {});
|
|
||||||
setSchedulePickerOpen(false);
|
|
||||||
}}>Aktivieren</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="ctrl-separator" />
|
|
||||||
<button
|
<button
|
||||||
className="ctrl-icon-btn ctrl-move"
|
className="ctrl-icon-btn ctrl-move"
|
||||||
title="Ausgewählte nach oben"
|
title="Ausgewählte nach oben"
|
||||||
@ -3592,17 +3530,7 @@ export function App(): ReactElement {
|
|||||||
<span className="stat-label">In Warteschlange</span>
|
<span className="stat-label">In Warteschlange</span>
|
||||||
<span className="stat-value">{itemStatusCounts.queued}</span>
|
<span className="stat-value">{itemStatusCounts.queued}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="stat-item">
|
||||||
className={`stat-item${itemStatusCounts.failed > 0 ? " stat-item-clickable" : ""}`}
|
|
||||||
title={itemStatusCounts.failed > 0 ? "Klicken zum Zurücksetzen aller fehlerhaften Downloads" : undefined}
|
|
||||||
onClick={() => {
|
|
||||||
if (itemStatusCounts.failed === 0) return;
|
|
||||||
const failedIds = Object.values(snapshot.session.items)
|
|
||||||
.filter((it) => it.status === "failed")
|
|
||||||
.map((it) => it.id);
|
|
||||||
void window.rd.resetItems(failedIds).catch(() => {});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="stat-label">Fehlerhaft</span>
|
<span className="stat-label">Fehlerhaft</span>
|
||||||
<span className="stat-value danger">{itemStatusCounts.failed}</span>
|
<span className="stat-value danger">{itemStatusCounts.failed}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -4638,14 +4566,6 @@ export function App(): ReactElement {
|
|||||||
{hasUrls && !multi && (
|
{hasUrls && !multi && (
|
||||||
<>
|
<>
|
||||||
<div className="ctx-menu-sep" />
|
<div className="ctx-menu-sep" />
|
||||||
<button className="ctx-menu-item" onClick={() => {
|
|
||||||
const rawText = contextEntry!.urls!.join("\n");
|
|
||||||
void window.rd.addLinks({ rawText, packageName: contextEntry!.name }).then((result) => {
|
|
||||||
if (result.addedLinks > 0) showToast(`${result.addedLinks} Link(s) zur Queue hinzugefügt`);
|
|
||||||
else showToast("Keine Links hinzugefügt");
|
|
||||||
}).catch(() => showToast("Fehler beim Hinzufügen"));
|
|
||||||
setHistoryCtxMenu(null);
|
|
||||||
}}>Erneut herunterladen</button>
|
|
||||||
<button className="ctx-menu-item" onClick={() => {
|
<button className="ctx-menu-item" onClick={() => {
|
||||||
const urls = contextEntry!.urls!;
|
const urls = contextEntry!.urls!;
|
||||||
const links = urls.map((u) => ({ name: u, url: u }));
|
const links = urls.map((u) => ({ name: u, url: u }));
|
||||||
|
|||||||
@ -365,67 +365,6 @@ body,
|
|||||||
background: rgba(245, 158, 11, 0.1);
|
background: rgba(245, 158, 11, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-ctrl {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.schedule-btn.active {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
.schedule-active {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.schedule-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--accent);
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 2px 6px;
|
|
||||||
background: rgba(99, 179, 237, 0.12);
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.schedule-cancel {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.schedule-cancel:hover { color: var(--danger, #e05555); }
|
|
||||||
.schedule-picker {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
left: 0;
|
|
||||||
z-index: 200;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
background: var(--panel);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
.schedule-picker-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.schedule-time-input {
|
|
||||||
background: var(--field);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--text);
|
|
||||||
padding: 3px 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
.ctrl-separator {
|
.ctrl-separator {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@ -2103,13 +2042,6 @@ td {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.stat-item.stat-item-clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
|
||||||
.stat-item.stat-item-clickable:hover {
|
|
||||||
border-color: var(--danger, #e05555);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@ -110,7 +110,6 @@ export interface AppSettings {
|
|||||||
autoExtractWhenStopped: boolean;
|
autoExtractWhenStopped: boolean;
|
||||||
disabledProviders: DebridProvider[];
|
disabledProviders: DebridProvider[];
|
||||||
hosterRouting: Record<string, DebridProvider>;
|
hosterRouting: Record<string, DebridProvider>;
|
||||||
scheduledStartEpochMs: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadItem {
|
export interface DownloadItem {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user