diff --git a/README.md b/README.md index f829114..40c7f2b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ API Media Player is a web-first PWA for browsing Hydrus media and handing playba ## What works today -- Browse the seeded demo library right after `npm install` and `npm run dev` -- Connect to a real Hydrus client from the Settings page or a local `.env` file +- Start the app with `npm start` and connect a Hydrus server from the web settings screen +- Add one or more Hydrus clients from the Settings page or a local `.env` file - Launch playback in native apps: - Windows and Linux desktop: `mpv` through `mpv-handler://` - Android: `mpv-android` through `intent://` @@ -23,12 +23,12 @@ npm install 3. Start the dev server: ```bash -npm run dev +npm start ``` 4. Open `http://localhost:5173`. -The app includes sample data, so you can verify the UI immediately. Playback itself is always external, so browsing works before player setup, but actual media launch depends on the platform-specific player flow below. +On a fresh install the app opens the Hydrus server settings first. Add your host, port, and API key there, test the connection, then save the server before browsing the library. Playback itself is always external, so actual media launch still depends on the platform-specific player flow below. ## Connect Hydrus (optional) @@ -41,7 +41,7 @@ VITE_HYDRUS_API_KEY= VITE_HYDRUS_SSL=false ``` -You can also add or edit servers from Settings inside the app. The first run seeds a sample server entry so you only need to supply your `Hydrus-Client-API-Access-Key` and test the connection. +You can also add or edit servers entirely from Settings inside the app. Nothing is preconfigured on first launch, so you enter the Hydrus connection details yourself. Browsers cannot attach custom Hydrus API headers to direct media URLs. If your Hydrus setup requires header-based authentication for file access, put a trusted reverse proxy in front of it or provide playable URLs another way. @@ -119,7 +119,7 @@ It only activates on `localhost`, loopback, RFC1918 LAN IPs, and `.local` or `.l ## Useful commands ```bash -npm run dev +npm start npm run build npm run preview npm run typecheck diff --git a/src/App.tsx b/src/App.tsx index 0e1694c..074a64e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,7 @@ import DownloadsOverlay, { type DownloadOverlayItem } from './components/Downloa import DownloadsPage from './pages/DownloadsPage' import { Box, CssBaseline, useMediaQuery } from '@mui/material' import { ThemeProvider } from '@mui/material/styles' -import { ServersProvider, useServers } from './context/ServersContext' +import { ACTIVE_KEY, STORAGE_KEY, ServersProvider, useServers } from './context/ServersContext' import { extractTitleFromTags, type HydrusFileDetails } from './api/hydrusClient' import { addDevLog } from './debugLog' import { clearStoredDownloads, deleteStoredDownload, listStoredDownloads, saveStoredDownload } from './downloadStore' @@ -367,9 +367,21 @@ function StartupLibraryCacheSync() { return null } +function getInitialActivePage(): MediaSection | 'settings' | 'downloads' { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return 'settings' + + const parsed = JSON.parse(raw) + return Array.isArray(parsed) && parsed.length > 0 ? 'audio' : 'settings' + } catch { + return 'settings' + } +} + function App() { const initialUiPreferences = useMemo(() => loadUiPreferences(), []) - const [activePage, setActivePage] = useState('audio') + const [activePage, setActivePage] = useState(() => getInitialActivePage()) const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true) const [libraryQuery, setLibraryQuery] = useState(initialUiPreferences.libraryQuery) @@ -452,6 +464,26 @@ function App() { } }, [activePage]) + useEffect(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY) + const parsed = raw ? JSON.parse(raw) : [] + const hasServers = Array.isArray(parsed) && parsed.length > 0 + + if (!hasServers && activePage !== 'settings') { + setActivePage('settings') + } + + if (!hasServers) { + localStorage.removeItem(ACTIVE_KEY) + } + } catch { + if (activePage !== 'settings') { + setActivePage('settings') + } + } + }, [activePage]) + useEffect(() => { saveUiPreferences({ libraryQuery }) }, [libraryQuery]) diff --git a/src/context/ServersContext.tsx b/src/context/ServersContext.tsx index b7be7dd..08124f5 100644 --- a/src/context/ServersContext.tsx +++ b/src/context/ServersContext.tsx @@ -3,8 +3,8 @@ import type { ServerConfig, ConnectivityResult } from '../api/hydrusClient' import { HydrusClient, makeId } from '../api/hydrusClient' import type { ServerSyncSummary } from '../types' -const STORAGE_KEY = 'hydrus_servers_v1' -const ACTIVE_KEY = 'hydrus_active_id_v1' +export const STORAGE_KEY = 'hydrus_servers_v1' +export const ACTIVE_KEY = 'hydrus_active_id_v1' export type Server = ServerConfig & { lastTest?: (ConnectivityResult & { timestamp: number }) | null @@ -73,28 +73,6 @@ export function ServersProvider({ children }: { children: React.ReactNode }) { setActiveServerIdState(servers[0].id) localStorage.setItem(ACTIVE_KEY, servers[0].id) } - - // Seed a local server if none exist yet (user-provided default IP) - if (servers.length === 0) { - const seedHost = '192.168.1.128' - const seedPort = '45869' - const seedName = 'Local Hydrus (192.168.1.128)' - const id = makeId() - const srv: Server = { - id, - name: seedName, - host: seedHost, - port: seedPort, - apiKey: '', - ssl: false, - forceApiKeyInQuery: false, - lastTest: { ok: false, message: 'Unauthenticated (401). Add API key to test', status: 401, searchOk: false, rangeSupported: false, timestamp: Date.now() } - } - setServers([srv]) - setActiveServerIdState(id) - localStorage.setItem(STORAGE_KEY, JSON.stringify([srv])) - localStorage.setItem(ACTIVE_KEY, id) - } }, []) const setActiveServerId = useCallback((id: string | null) => { diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index ff5b155..22ee3f0 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -50,6 +50,7 @@ type SettingsPageProps = { export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayEnabledChange, appTheme, onAppThemeChange, libraryDisplayMode, onLibraryDisplayModeChange, libraryPrimaryAction, onLibraryPrimaryActionChange }: SettingsPageProps) { const { servers, addServer, updateServer, removeServer, testServerById, testServerConfig, setActiveServerId, activeServerId } = useServers() + const isFirstServerSetup = servers.length === 0 const [editing, setEditing] = useState(null) const [form, setForm] = useState>(DEFAULT_SERVER_FORM) const [testing, setTesting] = useState(false) @@ -264,110 +265,124 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE - onClose && onClose()} aria-label="back" size="large" sx={{ mr: 1 }}> - - - Hydrus Servers + {!isFirstServerSetup && ( + onClose && onClose()} aria-label="back" size="large" sx={{ mr: 1 }}> + + + )} + + {isFirstServerSetup ? 'Connect to Hydrus' : 'Hydrus Servers'} + {isFirstServerSetup && ( + + Add your Hydrus Client API host, port, and API key before browsing the library. + + )} + - - - - Interface preferences - - Personalize the look and default actions without affecting your cached library. + {!isFirstServerSetup && ( + <> + + + + Interface preferences + + Personalize the look and default actions without affecting your cached library. + + + + + + + + Theme + + + + + Display + + + + + Track tap + + + + + + {APP_THEME_PRESETS.map((theme) => ( + + ))} + + + + {APP_THEME_PRESETS.find((theme) => theme.id === draftAppTheme)?.description} + + + {import.meta.env.DEV && ( + <> + Developer tools + + Control development-only UI that can get in the way on smaller screens. + + setDraftDevOverlayEnabled(event.target.checked)} />} + label={draftDevOverlayEnabled ? 'Floating dev overlay enabled' : 'Floating dev overlay disabled'} + sx={{ alignItems: 'flex-start', m: 0 }} + /> + + )} + + + + Library cache + + The app keeps one active IndexedDB snapshot and automatically removes older sync cache snapshots. + + + + + + + + + {cacheUpdatedAt ? `Cache updated: ${new Date(cacheUpdatedAt).toLocaleString()}` : 'Cache has not been written yet.'} - - - - - - Theme - - - - - Display - - - - - Track tap - - - - - - {APP_THEME_PRESETS.map((theme) => ( - - ))} - - - - {APP_THEME_PRESETS.find((theme) => theme.id === draftAppTheme)?.description} - - - {import.meta.env.DEV && ( - <> - Developer tools - - Control development-only UI that can get in the way on smaller screens. - - setDraftDevOverlayEnabled(event.target.checked)} />} - label={draftDevOverlayEnabled ? 'Floating dev overlay enabled' : 'Floating dev overlay disabled'} - sx={{ alignItems: 'flex-start', m: 0 }} - /> - - )} - - - - Library cache - - The app keeps one active IndexedDB snapshot and automatically removes older sync cache snapshots. - - - - - - - - - {cacheUpdatedAt ? `Cache updated: ${new Date(cacheUpdatedAt).toLocaleString()}` : 'Cache has not been written yet.'} - - + + )} + {!isFirstServerSetup && ( @@ -423,14 +438,23 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE + )} - + - {editing ? 'Edit server' : 'Add new server'} + {editing ? 'Edit server' : isFirstServerSetup ? 'Add your first server' : 'Add new server'} + + Use the same host, port, and API key you use for the Hydrus Client API. + + {isFirstServerSetup && ( + + Nothing is preconfigured. Enter your Hydrus server details below, test the connection, then save the server to unlock the library. + + )} setForm({ ...form, name: e.target.value })} fullWidth /> - setForm({ ...form, host: e.target.value })} fullWidth /> - setForm({ ...form, port: e.target.value })} fullWidth /> + setForm({ ...form, host: e.target.value })} fullWidth /> + setForm({ ...form, port: e.target.value })} fullWidth /> setForm({ ...form, apiKey: e.target.value })} fullWidth /> setForm({ ...form, ssl: e.target.checked })} />} label="Use HTTPS (SSL)" /> setForm({ ...form, forceApiKeyInQuery: e.target.checked })} />} label="Send API key in query parameter" />