Background running for downloads/auto-download, library clipboard detect, updater auth + token
Closing the window no longer kills in-progress downloads, the library gains the same copied-link suggestion the downloads tab has, and the in-app updater can now authenticate to a sign-in-required Gitea. Background / tray: - The window's close handler now hides to the tray (instead of quitting) whenever a download is in flight, even if "Keep running in the tray" is off — quitting was killing the spawned yt-dlp processes. A one-time notification explains the app is still running. (download.ts exposes hasActiveDownloads().) - tray.ts now falls back to an embedded icon when no build/icon.ico ships, so the tray actually appears — previously an empty icon meant the tray was skipped and a minimized window could be stranded with no way back. - New "Start with Windows" setting (launchAtStartup) wired to app.setLoginItemSettings, synced at startup and on toggle. Useful with auto-download so watched channels stay current in the background. - The "Keep running in the tray" hint now explains it also enables background auto-download of new uploads. Library clipboard detection: - Extracted the downloads tab's clipboard watcher into a shared useClipboardLink hook and used it in the library's add-source field, so a copied channel/playlist link is offered there too. Updater fix (works on a private / sign-in-required instance): - Added an optional updateToken setting. When set, the updater sends it as a Gitea Authorization header on the release check, the checksum fetch, and the installer download — so "Check for updates" works where anonymous access is blocked (the previous "could not reach the update server" case). Blank = anonymous, unchanged. No token is ever shipped; it's only ever sent to the host-pinned update host. Settings gains a masked "Update access token" field. Typecheck clean; 187 tests pass; electron-vite build clean. New UI verified in the browser preview (tray/startup toggles, token field, library copied-link banner). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-1
@@ -3,6 +3,7 @@ import { createWriteStream, type WriteStream } from 'fs'
|
||||
import { stat, unlink } from 'fs/promises'
|
||||
import { join, normalize, dirname } from 'path'
|
||||
import { createHash } from 'crypto'
|
||||
import { getSettings } from './settings'
|
||||
import {
|
||||
IpcChannels,
|
||||
type AppUpdateInfo,
|
||||
@@ -10,6 +11,18 @@ import {
|
||||
type AppUpdateProgress
|
||||
} from '@shared/ipc'
|
||||
|
||||
/**
|
||||
* Authorization header for the update host. Empty unless the user has set an
|
||||
* updateToken in Settings — needed when the release repo is private or the Gitea
|
||||
* instance requires sign-in for anonymous access (the default on this instance).
|
||||
* Only ever sent to the host-pinned UPDATE_HOST (see isTrustedDownloadUrl), so a
|
||||
* redirect can never leak the token to another origin.
|
||||
*/
|
||||
function authHeader(): Record<string, string> {
|
||||
const tok = getSettings().updateToken?.trim()
|
||||
return tok ? { Authorization: `token ${tok}` } : {}
|
||||
}
|
||||
|
||||
// --- Update source -----------------------------------------------------------
|
||||
// The Gitea repo whose Releases host the AeroFetch installers. The updater reads
|
||||
// the repo's latest release over the public REST API and downloads the installer
|
||||
@@ -109,7 +122,7 @@ export async function checkForAppUpdate(): Promise<AppUpdateInfo> {
|
||||
const timer = setTimeout(() => controller.abort(), 15_000)
|
||||
try {
|
||||
const res = await fetch(RELEASE_API, {
|
||||
headers: { Accept: 'application/json' },
|
||||
headers: { Accept: 'application/json', ...authHeader() },
|
||||
signal: controller.signal
|
||||
})
|
||||
if (!res.ok) {
|
||||
@@ -187,6 +200,7 @@ function fetchTrustedText(
|
||||
resolve(r)
|
||||
}
|
||||
const request = net.request({ url, redirect: 'manual' })
|
||||
for (const [k, v] of Object.entries(authHeader())) request.setHeader(k, v)
|
||||
const timer = setTimeout(() => done({ ok: false, error: 'timed out' }), timeoutMs)
|
||||
request.on('redirect', (_s, _m, redirectUrl) => {
|
||||
if (!isTrustedDownloadUrl(redirectUrl)) {
|
||||
@@ -279,6 +293,7 @@ export async function downloadAppUpdate(url: string, wc: WebContents): Promise<A
|
||||
// fetch hides the Location header under redirect:'manual', so it can't gate
|
||||
// hops. 'manual' here means a hop only proceeds if we call followRedirect().
|
||||
const request = net.request({ url, redirect: 'manual' })
|
||||
for (const [k, v] of Object.entries(authHeader())) request.setHeader(k, v)
|
||||
|
||||
const armIdle = (): void => {
|
||||
if (idle) clearTimeout(idle)
|
||||
|
||||
Reference in New Issue
Block a user