Complete Phase E theming, onboarding, and Windows share integration
Adds theme presets (Toffee/Slate/Evergreen/Lavender) with a "follow system" mode and high-contrast awareness, a first-run welcome screen, and the Windows analog of Android's share sheet for a Win32 app: an aerofetch:// protocol handler plus an Explorer "Send to AeroFetch" entry, both routed through a single-instance lock so a second launch hands its link to the already-running window instead of opening a duplicate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { app, shell, type BrowserWindow } from 'electron'
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
/** Only ever forward http(s) targets into the app — same restriction the
|
||||
* external-link window-open handler in index.ts applies to in-page links. */
|
||||
function asHttpUrl(candidate: string): string | null {
|
||||
try {
|
||||
const u = new URL(candidate)
|
||||
return u.protocol === 'http:' || u.protocol === 'https:' ? candidate : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a Windows Internet Shortcut (.url) file's target — what Explorer's
|
||||
* "Send to" menu hands us when the user sends a saved link to AeroFetch. */
|
||||
function readUrlShortcut(path: string): string | null {
|
||||
try {
|
||||
const text = readFileSync(path, 'utf8')
|
||||
const match = /^URL=(.+)$/im.exec(text)
|
||||
return match ? asHttpUrl(match[1].trim()) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls a download target out of process argv — either the `aerofetch://`
|
||||
* custom protocol (`aerofetch://download?url=<encoded>`) or a `.url` Internet
|
||||
* Shortcut path Explorer's "Send to" menu passes us (see registerSendToShortcut).
|
||||
*/
|
||||
export function extractIncomingUrl(argv: string[]): string | null {
|
||||
for (const arg of argv) {
|
||||
if (arg.startsWith('aerofetch://')) {
|
||||
try {
|
||||
const target = new URL(arg).searchParams.get('url')
|
||||
const valid = target && asHttpUrl(target)
|
||||
if (valid) return valid
|
||||
} catch {
|
||||
/* malformed protocol invocation — ignore */
|
||||
}
|
||||
} else if (/\.url$/i.test(arg) && existsSync(arg)) {
|
||||
const target = readUrlShortcut(arg)
|
||||
if (target) return target
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds/refreshes a "Send to AeroFetch" entry in Explorer's SendTo menu — the
|
||||
* closest Windows analog to Android's share sheet a Win32 (non-MSIX) app can
|
||||
* offer; registering as an actual Share Target requires an MSIX package.
|
||||
* Best-effort and idempotent (re-running just overwrites the same shortcut),
|
||||
* so a failure here should never block startup.
|
||||
*/
|
||||
export function registerSendToShortcut(): void {
|
||||
if (process.platform !== 'win32') return
|
||||
try {
|
||||
const shortcutPath = join(
|
||||
app.getPath('appData'),
|
||||
'Microsoft',
|
||||
'Windows',
|
||||
'SendTo',
|
||||
'AeroFetch.lnk'
|
||||
)
|
||||
shell.writeShortcutLink(shortcutPath, {
|
||||
target: process.execPath,
|
||||
description: 'Send to AeroFetch',
|
||||
icon: process.execPath,
|
||||
iconIndex: 0
|
||||
})
|
||||
} catch {
|
||||
/* SendTo integration is a nice-to-have */
|
||||
}
|
||||
}
|
||||
|
||||
/** Focuses (and un-minimizes) an existing window — used when a second launch
|
||||
* (protocol or SendTo) should hand its URL to the already-running instance. */
|
||||
export function focusWindow(win: BrowserWindow): void {
|
||||
if (win.isMinimized()) win.restore()
|
||||
win.show()
|
||||
win.focus()
|
||||
}
|
||||
Reference in New Issue
Block a user