change installation process

This commit is contained in:
2026-04-18 18:33:10 -07:00
parent 3bd335cfb5
commit e9c72d13df
4 changed files with 167 additions and 133 deletions
+125 -101
View File
@@ -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<Server | null>(null)
const [form, setForm] = useState<Omit<Server, 'id' | 'lastTest'>>(DEFAULT_SERVER_FORM)
const [testing, setTesting] = useState(false)
@@ -264,110 +265,124 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
<Box sx={{ width: '100%', maxWidth: 1280, mx: 'auto' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton onClick={() => onClose && onClose()} aria-label="back" size="large" sx={{ mr: 1 }}>
<ArrowBackIcon />
</IconButton>
<Typography variant="h6">Hydrus Servers</Typography>
{!isFirstServerSetup && (
<IconButton onClick={() => onClose && onClose()} aria-label="back" size="large" sx={{ mr: 1 }}>
<ArrowBackIcon />
</IconButton>
)}
<Box>
<Typography variant="h6">{isFirstServerSetup ? 'Connect to Hydrus' : 'Hydrus Servers'}</Typography>
{isFirstServerSetup && (
<Typography variant="body2" color="text.secondary">
Add your Hydrus Client API host, port, and API key before browsing the library.
</Typography>
)}
</Box>
</Box>
</Box>
<Box sx={{ border: '1px solid rgba(255,255,255,0.08)', borderRadius: 2, bgcolor: 'background.paper', p: { xs: 1.5, sm: 2 }, mb: { xs: 2, lg: 3 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2, mb: 1.5, flexWrap: 'wrap' }}>
<Box>
<Typography variant="subtitle1" sx={{ mb: 0.5 }}>Interface preferences</Typography>
<Typography variant="body2" color="text.secondary">
Personalize the look and default actions without affecting your cached library.
{!isFirstServerSetup && (
<>
<Box sx={{ border: '1px solid rgba(255,255,255,0.08)', borderRadius: 2, bgcolor: 'background.paper', p: { xs: 1.5, sm: 2 }, mb: { xs: 2, lg: 3 } }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2, mb: 1.5, flexWrap: 'wrap' }}>
<Box>
<Typography variant="subtitle1" sx={{ mb: 0.5 }}>Interface preferences</Typography>
<Typography variant="body2" color="text.secondary">
Personalize the look and default actions without affecting your cached library.
</Typography>
</Box>
<Button variant="contained" onClick={handleSavePreferences} disabled={!preferencesDirty}>
Save preferences
</Button>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: 'repeat(3, minmax(0, 240px))' }, gap: 1.5, mb: import.meta.env.DEV ? 2 : 0 }}>
<FormControl size="small">
<InputLabel id="settings-app-theme-label">Theme</InputLabel>
<Select
labelId="settings-app-theme-label"
value={draftAppTheme}
label="Theme"
onChange={(event) => setDraftAppTheme(event.target.value as AppThemeId)}
>
{APP_THEME_PRESETS.map((theme) => (
<MenuItem key={theme.id} value={theme.id}>{theme.label}</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small">
<InputLabel id="settings-library-display-mode-label">Display</InputLabel>
<Select
labelId="settings-library-display-mode-label"
value={draftLibraryDisplayMode}
label="Display"
onChange={(event) => setDraftLibraryDisplayMode(event.target.value as 'grid' | 'table')}
>
<MenuItem value="grid">Grid</MenuItem>
<MenuItem value="table">Table</MenuItem>
</Select>
</FormControl>
<FormControl size="small">
<InputLabel id="settings-library-primary-action-label">Track tap</InputLabel>
<Select
labelId="settings-library-primary-action-label"
value={draftLibraryPrimaryAction}
label="Track tap"
onChange={(event) => setDraftLibraryPrimaryAction(event.target.value as LibraryPrimaryAction)}
>
<MenuItem value="details">Open details popup</MenuItem>
<MenuItem value="stream">Stream immediately</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: import.meta.env.DEV ? 2 : 0.5 }}>
{APP_THEME_PRESETS.map((theme) => (
<Chip key={theme.id} label={theme.label} color={draftAppTheme === theme.id ? 'primary' : 'default'} variant={draftAppTheme === theme.id ? 'filled' : 'outlined'} />
))}
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: import.meta.env.DEV ? 0.5 : 0 }}>
{APP_THEME_PRESETS.find((theme) => theme.id === draftAppTheme)?.description}
</Typography>
{import.meta.env.DEV && (
<>
<Typography variant="subtitle1" sx={{ mb: 0.5 }}>Developer tools</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
Control development-only UI that can get in the way on smaller screens.
</Typography>
<FormControlLabel
control={<Switch checked={draftDevOverlayEnabled} onChange={(event) => setDraftDevOverlayEnabled(event.target.checked)} />}
label={draftDevOverlayEnabled ? 'Floating dev overlay enabled' : 'Floating dev overlay disabled'}
sx={{ alignItems: 'flex-start', m: 0 }}
/>
</>
)}
</Box>
<Box sx={{ border: '1px solid rgba(255,255,255,0.08)', borderRadius: 2, bgcolor: 'background.paper', p: { xs: 1.5, sm: 2 }, mb: { xs: 2, lg: 3 } }}>
<Typography variant="subtitle1" sx={{ mb: 0.5 }}>Library cache</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
The app keeps one active IndexedDB snapshot and automatically removes older sync cache snapshots.
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip label={`Storage: ${cacheStorageText}`} size="small" color="primary" variant="outlined" />
<Chip label={`Tracks: ${cacheTrackCount}`} size="small" variant="outlined" />
<Chip label={`Searches: ${cacheSearchCount}`} size="small" variant="outlined" />
<Chip label={`Snapshots kept: ${cacheSnapshotCount}`} size="small" variant="outlined" />
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
{cacheUpdatedAt ? `Cache updated: ${new Date(cacheUpdatedAt).toLocaleString()}` : 'Cache has not been written yet.'}
</Typography>
</Box>
<Button variant="contained" onClick={handleSavePreferences} disabled={!preferencesDirty}>
Save preferences
</Button>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: 'repeat(3, minmax(0, 240px))' }, gap: 1.5, mb: import.meta.env.DEV ? 2 : 0 }}>
<FormControl size="small">
<InputLabel id="settings-app-theme-label">Theme</InputLabel>
<Select
labelId="settings-app-theme-label"
value={draftAppTheme}
label="Theme"
onChange={(event) => setDraftAppTheme(event.target.value as AppThemeId)}
>
{APP_THEME_PRESETS.map((theme) => (
<MenuItem key={theme.id} value={theme.id}>{theme.label}</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small">
<InputLabel id="settings-library-display-mode-label">Display</InputLabel>
<Select
labelId="settings-library-display-mode-label"
value={draftLibraryDisplayMode}
label="Display"
onChange={(event) => setDraftLibraryDisplayMode(event.target.value as 'grid' | 'table')}
>
<MenuItem value="grid">Grid</MenuItem>
<MenuItem value="table">Table</MenuItem>
</Select>
</FormControl>
<FormControl size="small">
<InputLabel id="settings-library-primary-action-label">Track tap</InputLabel>
<Select
labelId="settings-library-primary-action-label"
value={draftLibraryPrimaryAction}
label="Track tap"
onChange={(event) => setDraftLibraryPrimaryAction(event.target.value as LibraryPrimaryAction)}
>
<MenuItem value="details">Open details popup</MenuItem>
<MenuItem value="stream">Stream immediately</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: import.meta.env.DEV ? 2 : 0.5 }}>
{APP_THEME_PRESETS.map((theme) => (
<Chip key={theme.id} label={theme.label} color={draftAppTheme === theme.id ? 'primary' : 'default'} variant={draftAppTheme === theme.id ? 'filled' : 'outlined'} />
))}
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: import.meta.env.DEV ? 0.5 : 0 }}>
{APP_THEME_PRESETS.find((theme) => theme.id === draftAppTheme)?.description}
</Typography>
{import.meta.env.DEV && (
<>
<Typography variant="subtitle1" sx={{ mb: 0.5 }}>Developer tools</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
Control development-only UI that can get in the way on smaller screens.
</Typography>
<FormControlLabel
control={<Switch checked={draftDevOverlayEnabled} onChange={(event) => setDraftDevOverlayEnabled(event.target.checked)} />}
label={draftDevOverlayEnabled ? 'Floating dev overlay enabled' : 'Floating dev overlay disabled'}
sx={{ alignItems: 'flex-start', m: 0 }}
/>
</>
)}
</Box>
<Box sx={{ border: '1px solid rgba(255,255,255,0.08)', borderRadius: 2, bgcolor: 'background.paper', p: { xs: 1.5, sm: 2 }, mb: { xs: 2, lg: 3 } }}>
<Typography variant="subtitle1" sx={{ mb: 0.5 }}>Library cache</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
The app keeps one active IndexedDB snapshot and automatically removes older sync cache snapshots.
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip label={`Storage: ${cacheStorageText}`} size="small" color="primary" variant="outlined" />
<Chip label={`Tracks: ${cacheTrackCount}`} size="small" variant="outlined" />
<Chip label={`Searches: ${cacheSearchCount}`} size="small" variant="outlined" />
<Chip label={`Snapshots kept: ${cacheSnapshotCount}`} size="small" variant="outlined" />
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
{cacheUpdatedAt ? `Cache updated: ${new Date(cacheUpdatedAt).toLocaleString()}` : 'Cache has not been written yet.'}
</Typography>
</Box>
</>
)}
<Grid container spacing={{ xs: 2, lg: 3 }} alignItems="flex-start">
{!isFirstServerSetup && (
<Grid item xs={12} lg={5}>
<Box sx={{ border: '1px solid rgba(255,255,255,0.08)', borderRadius: 2, bgcolor: 'background.paper', p: { xs: 1.5, sm: 2 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
@@ -423,14 +438,23 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
</List>
</Box>
</Grid>
)}
<Grid item xs={12} lg={7}>
<Grid item xs={12} lg={isFirstServerSetup ? 8 : 7} sx={isFirstServerSetup ? { mx: 'auto' } : undefined}>
<Box sx={{ border: '1px solid rgba(255,255,255,0.08)', borderRadius: 2, bgcolor: 'background.paper', p: { xs: 1.5, sm: 2 } }}>
<Typography variant="subtitle1">{editing ? 'Edit server' : 'Add new server'}</Typography>
<Typography variant="subtitle1">{editing ? 'Edit server' : isFirstServerSetup ? 'Add your first server' : 'Add new server'}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
Use the same host, port, and API key you use for the Hydrus Client API.
</Typography>
{isFirstServerSetup && (
<Alert severity="info" sx={{ mt: 2 }}>
Nothing is preconfigured. Enter your Hydrus server details below, test the connection, then save the server to unlock the library.
</Alert>
)}
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField label="Name (optional)" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} fullWidth />
<TextField label="Host (IP or host, e.g. 192.168.1.128)" value={form.host} onChange={(e) => setForm({ ...form, host: e.target.value })} fullWidth />
<TextField label="Port (optional)" value={form.port as any ?? ''} onChange={(e) => setForm({ ...form, port: e.target.value })} fullWidth />
<TextField label="Host or IP" placeholder="192.168.1.128 or hydrus.local" value={form.host} onChange={(e) => setForm({ ...form, host: e.target.value })} fullWidth />
<TextField label="Port" placeholder="45869" value={form.port as any ?? ''} onChange={(e) => setForm({ ...form, port: e.target.value })} fullWidth />
<TextField label="API Key (optional)" value={form.apiKey} onChange={(e) => setForm({ ...form, apiKey: e.target.value })} fullWidth />
<FormControlLabel control={<Switch checked={!!form.ssl} onChange={(e) => setForm({ ...form, ssl: e.target.checked })} />} label="Use HTTPS (SSL)" />
<FormControlLabel control={<Switch checked={!!form.forceApiKeyInQuery} onChange={(e) => setForm({ ...form, forceApiKeyInQuery: e.target.checked })} />} label="Send API key in query parameter" />