feat(web): storage admin, search and catalog pages (M2)
- Storage admin: local folders with a folder picker and NFS sources, connection test, scan and full scan with live progress, enable, disable, delete - path pattern editor with reorder, examples and a preview table that shows which pattern matched and what came from tags; compile errors land under the broken pattern - Library page with counters, scans and unreadable files; search reindex for admins - search box in the top bar: combobox with keyboard navigation, 150 ms debounce, cancelled stale requests, "/" and Ctrl+K, layout fix hint - search page with tabs and paging, artist and album pages, album cards and track lists; unavailable tracks are dimmed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c1160cccd3
commit
a685b1b7ca
27 files changed
+2505
-9
No files matched your search
@@ -0,0 +1,44 @@
|
||||
import type { components } from '@pipu/api-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { formatDateTime } from '../../shared/lib/format';
|
||||
import { cx } from '../../shared/ui/ui';
|
||||
import s from './library.module.css';
|
||||
|
||||
type ScanProgress = components['schemas']['ScanProgress'];
|
||||
export type SourceStatus = components['schemas']['SourceStatus'];
|
||||
|
||||
export function StatusBadge({ status }: { status: SourceStatus }) {
|
||||
const { t } = useTranslation('admin');
|
||||
return <span className={cx(s.status, s[`st_${status}`])}>{t(`sources.status_${status}`)}</span>;
|
||||
}
|
||||
|
||||
/** Прогресс идущего скана или итог последнего. */
|
||||
export function ScanLine({ scan, lastScanAt }: { scan: ScanProgress | null; lastScanAt: string | null }) {
|
||||
const { t, i18n } = useTranslation('admin');
|
||||
if (scan?.running) {
|
||||
const pct = scan.to_process > 0 ? Math.round((scan.processed / scan.to_process) * 100) : 0;
|
||||
return (
|
||||
<div className={s.scan}>
|
||||
<div className={s.scanText}>
|
||||
{scan.found === 0 ? t('sources.scan_walking') : t('sources.scanning', { processed: scan.processed, total: scan.to_process })}
|
||||
</div>
|
||||
<div className={s.bar} role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100}>
|
||||
<span style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={s.scan}>
|
||||
<div className={s.scanText}>
|
||||
{lastScanAt ? t('sources.last_scan', { date: formatDateTime(lastScanAt, i18n.language) }) : t('sources.never_scanned')}
|
||||
</div>
|
||||
{scan?.error ? (
|
||||
<div className={s.scanError}>{t('sources.scan_failed', { error: scan.error })}</div>
|
||||
) : (
|
||||
scan && <div className={s.scanSummary}>{t('sources.scan_done', { ...scan })}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user