import './assets/base.css' import React from 'react' import ReactDOM from 'react-dom/client' import { DEFAULT_DOWNLOAD_OPTIONS, type Settings, type CommandTemplate } from '@shared/ipc' import App from './App' // In the standalone UI preview (browser, no Electron preload) window.api isn't // injected. Stub the full surface so the UI renders and stays interactive during // design work. The store detects preview mode (no window.electron) and drives a // fake progress ticker instead of calling these, so most are inert no-ops. // In the real Electron app this branch is skipped — preload provides window.api. if (import.meta.env.DEV && !window.api) { const MOCK_SETTINGS: Settings = { videoDir: 'C:\\Users\\you\\Documents\\Video', audioDir: 'C:\\Users\\you\\Documents\\Audio', defaultKind: 'video', defaultVideoQuality: 'Best available', defaultAudioQuality: 'Best (MP3)', maxConcurrent: 2, filenameTemplate: '%(title)s.%(ext)s', theme: 'light', accentColor: 'teal', clipboardWatch: true, downloadOptions: DEFAULT_DOWNLOAD_OPTIONS, proxy: '', rateLimit: '', useAria2c: false, cookieSource: 'none', cookiesBrowser: 'chrome', youtubePlayerClient: '', youtubePoToken: '', restrictFilenames: false, downloadArchive: false, autoUpdateYtdlp: true, ytdlpChannel: 'nightly', ytdlpLastUpdateCheck: Date.now() - 3_600_000, customCommandEnabled: false, defaultTemplateId: null, notifyOnComplete: true, autoDownloadNew: true, hasCompletedOnboarding: true, minimizeToTray: false, launchAtStartup: false, updateToken: '' } // Stands in for the cookies.txt file's mtime — lets the Cookies card's // sign-in/clear flow be exercised in this browser-only preview. let mockCookiesSavedAt: number | null = null // Stands in for templates.json — lets the Custom commands card's CRUD and // the download bar's template picker be exercised in this browser-only preview. let mockTemplates: CommandTemplate[] = [ { id: 'tpl1', name: 'Write thumbnail, no mtime', args: '--write-thumbnail --no-mtime' } ] window.api = { getAppVersion: async () => '0.4.0-preview', checkForAppUpdate: async () => { await new Promise((r) => setTimeout(r, 400)) return { ok: true, available: true, currentVersion: '0.4.0', latestVersion: '0.5.0', notes: '### What’s new in v0.5.0\n- In-app updater (this screen!)\n- Faster downloads\n- Bug fixes', htmlUrl: 'https://gitea.netbird.zimspace.uk:5938/debont80/AeroFetch/releases', downloadUrl: 'https://gitea.netbird.zimspace.uk:5938/example/AeroFetch-Setup-0.5.0.exe', assetName: 'AeroFetch-Setup-0.5.0.exe' } }, downloadAppUpdate: async () => ({ ok: false, error: 'Downloading updates is disabled in the browser preview.' }), runAppUpdate: async () => ({ ok: true }), onAppUpdateProgress: () => () => {}, getYtdlpVersion: async () => ({ ok: true, version: '2025.06.01 (UI preview mock)' }), getFfmpegVersions: async () => ({ ffmpeg: '7.1-full_build (UI preview mock)', ffprobe: '7.1-full_build (UI preview mock)' }), probe: async (url: string) => { await new Promise((r) => setTimeout(r, 700)) // simulate network latency // A 'list'/'playlist' URL exercises the playlist selection UI in preview. if (/list=|playlist/i.test(url)) { return { ok: true, kind: 'playlist', playlist: { title: 'Full Electron Course — All Episodes', uploader: 'DevChannel', count: 5, entries: Array.from({ length: 5 }, (_, i) => ({ index: i + 1, id: `vid${i + 1}`, title: `Episode ${i + 1} — ${['Setup', 'Main Process', 'IPC', 'Packaging', 'Release'][i]}`, url: `https://youtube.com/watch?v=vid${i + 1}`, durationLabel: `${10 + i}:0${i}`, uploader: 'DevChannel' })) } } } return { ok: true, kind: 'video', info: { title: 'Building a Desktop App with Electron — Full Course', channel: 'DevChannel', durationLabel: '1:42:08', thumbnail: undefined, formats: [ { id: '__best__', label: 'Best available', ext: 'mp4', hasAudio: true }, { id: '299', label: '1080p60 · mp4 · 248 MB', height: 1080, ext: 'mp4', filesizeLabel: '248 MB', hasAudio: false }, { id: '136', label: '720p · mp4 · 124 MB', height: 720, ext: 'mp4', filesizeLabel: '124 MB', hasAudio: false }, { id: '135', label: '480p · mp4 · 72 MB', height: 480, ext: 'mp4', filesizeLabel: '72 MB', hasAudio: false }, { id: '134', label: '360p · mp4 · 38 MB', height: 360, ext: 'mp4', filesizeLabel: '38 MB', hasAudio: false } ] } } }, startDownload: async () => ({ ok: true }), cancelDownload: async () => {}, pauseDownload: async () => {}, getDefaultFolder: async () => 'C:\\Users\\you\\Downloads', chooseFolder: async () => null, openPath: async () => '', showInFolder: async () => {}, readClipboard: async () => '', getSettings: async () => MOCK_SETTINGS, setSettings: async (partial) => Object.assign(MOCK_SETTINGS, partial), listHistory: async () => [], addHistory: async () => [], removeHistory: async () => [], removeManyHistory: async () => [], clearHistory: async () => [], cookiesLogin: async () => { await new Promise((r) => setTimeout(r, 600)) // simulate the sign-in window mockCookiesSavedAt = Date.now() return { ok: true, cookieCount: 14 } }, cookiesStatus: async () => mockCookiesSavedAt ? { exists: true, savedAt: mockCookiesSavedAt } : { exists: false }, cookiesClear: async () => { mockCookiesSavedAt = null }, listTemplates: async () => mockTemplates, saveTemplate: async (template) => { mockTemplates = [template, ...mockTemplates.filter((t) => t.id !== template.id)] return mockTemplates }, removeTemplate: async (id) => { mockTemplates = mockTemplates.filter((t) => t.id !== id) return mockTemplates }, previewCommand: async (opts) => { await new Promise((r) => setTimeout(r, 300)) // simulate the main-process round-trip const extra = opts.extraArgs ? ` ${opts.extraArgs}` : '' const dir = opts.kind === 'audio' ? MOCK_SETTINGS.audioDir : MOCK_SETTINGS.videoDir return { ok: true, command: `yt-dlp.exe -f "bv*+ba/b" -o "${dir}\\%(title)s.%(ext)s"` + `${extra} -- ${opts.url}` } }, updateYtdlp: async (channel) => { await new Promise((r) => setTimeout(r, 600)) // simulate the self-update run return { ok: true, output: `yt-dlp is already up to date (channel: ${channel}, UI preview mock)` } }, // No background updater in the browser preview — hand back an inert unsubscribe. onYtdlpAutoUpdateStatus: () => () => {}, listErrorLog: async () => [], clearErrorLog: async () => [], exportBackup: async () => ({ ok: true, path: 'C:\\Users\\you\\Downloads\\aerofetch-backup.json' }), importBackup: async () => ({ ok: true }), onDownloadEvent: () => () => {}, getSystemTheme: async () => ({ shouldUseDarkColors: false, shouldUseHighContrastColors: false }), onSystemThemeUpdate: () => () => {}, openHighContrastSettings: async () => {}, onExternalUrl: () => () => {}, // Media-manager sources — a seeded channel so the (Phase H) Library view has // demo data in this browser-only preview. listSources: async () => [ { id: 'src-demo', url: 'https://www.youtube.com/@DevChannel', kind: 'channel', title: 'DevChannel', channel: 'DevChannel', addedAt: Date.now() - 86_400_000, lastIndexedAt: Date.now() - 3_600_000, itemCount: 7 } ], indexSource: async (url) => { await new Promise((r) => setTimeout(r, 800)) // simulate the index walk return { ok: true, source: { id: 'src-demo', url, kind: 'channel', title: 'DevChannel', channel: 'DevChannel', addedAt: Date.now(), lastIndexedAt: Date.now(), itemCount: 7 }, itemCount: 7 } }, reindexSource: async () => ({ ok: true, itemCount: 7, newCount: 0 }), removeSource: async () => [], markSourceItemDownloaded: async () => [], setSourceWatched: async () => [], syncSources: async () => ({ ok: true, newItems: [] }), getScheduledSync: async () => ({ enabled: false }), setScheduledSync: async (enabled: boolean) => ({ enabled }), listSourceItems: async (sourceId) => Array.from({ length: 7 }, (_, i) => ({ id: `${sourceId}:vid${i + 1}`, sourceId, videoId: `vid${i + 1}`, title: `Episode ${i + 1}`, url: `https://youtube.com/watch?v=vid${i + 1}`, playlistTitle: i < 5 ? 'Full Electron Course' : 'Uploads', playlistIndex: i < 5 ? i + 1 : i - 4, durationLabel: `${10 + i}:0${i}`, downloaded: i < 2 })), onIndexProgress: () => () => {}, runTerminal: async () => ({ ok: true }), cancelTerminal: async () => {}, onTerminalOutput: () => () => {}, setTaskbarProgress: () => {} } } ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( )