e29374dcfa
The ROADMAP/ROADMAP-PINCHFLAT docs were removed; this drops the now-dead "see ROADMAP-PINCHFLAT.md" breadcrumbs (and one "the roadmap" mention) from source comments, preserving the surrounding Pinchflat-style context. Comment-only; typecheck (node+web) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
import { useState } from 'react'
|
|
import {
|
|
Button,
|
|
Field,
|
|
SpinButton,
|
|
Caption1,
|
|
makeStyles,
|
|
tokens,
|
|
shorthands
|
|
} from '@fluentui/react-components'
|
|
import { DeleteRegular, ArrowUpRegular } from '@fluentui/react-icons'
|
|
import { useSources } from '../store/sources'
|
|
import { toast } from '../store/toasts'
|
|
import { SPACE } from './ui/tokens'
|
|
|
|
const useStyles = makeStyles({
|
|
root: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '8px',
|
|
padding: SPACE.cozy,
|
|
backgroundColor: tokens.colorNeutralBackground2,
|
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
|
border: `1px solid ${tokens.colorNeutralStroke2}`
|
|
},
|
|
heading: { fontWeight: tokens.fontWeightSemibold },
|
|
controls: { display: 'flex', alignItems: 'flex-end', gap: '12px', flexWrap: 'wrap' },
|
|
spacer: { flexGrow: 1 },
|
|
muted: { color: tokens.colorNeutralForeground3 },
|
|
narrow: { width: '120px' }
|
|
})
|
|
|
|
/**
|
|
* Retention / quality-upgrade panel for one Library source (PINCHFLAT stretch).
|
|
* Two opt-in maintenance actions:
|
|
*
|
|
* - Prune: keep only the N most-recent (and/or last-D-days) downloaded files;
|
|
* delete the older ones' files (the download archive stops them silently
|
|
* re-downloading). Destructive, so it shows a dry-run count and a two-step
|
|
* confirm before any file is deleted.
|
|
* - Upgrade: re-download items that were fetched below the source's current
|
|
* target quality (from its profile or the global default).
|
|
*
|
|
* The policy inputs are ephemeral (per-open) — this is an on-demand tidy, not a
|
|
* persisted schedule, keeping retention to a desktop-app-sized scope.
|
|
*/
|
|
export function SourceRetention({ sourceId }: { sourceId: string }): React.JSX.Element {
|
|
const styles = useStyles()
|
|
const prunePreview = useSources((s) => s.prunePreview)
|
|
const pruneItems = useSources((s) => s.pruneItems)
|
|
const upgradePreview = useSources((s) => s.upgradePreview)
|
|
const upgradeItems = useSources((s) => s.upgradeItems)
|
|
|
|
const [maxCount, setMaxCount] = useState(25)
|
|
const [confirmIds, setConfirmIds] = useState<string[] | null>(null)
|
|
|
|
function onPrune(): void {
|
|
const candidates = prunePreview(sourceId, { maxCount })
|
|
if (candidates.length === 0) {
|
|
toast(`Nothing to prune — the source has ${maxCount} or fewer downloaded files.`, 'info')
|
|
return
|
|
}
|
|
setConfirmIds(candidates.map((c) => c.id))
|
|
}
|
|
|
|
async function onConfirmPrune(): Promise<void> {
|
|
if (!confirmIds) return
|
|
const deleted = await pruneItems(sourceId, confirmIds)
|
|
toast(`Pruned ${deleted} file${deleted === 1 ? '' : 's'}.`, 'info')
|
|
setConfirmIds(null)
|
|
}
|
|
|
|
function onUpgrade(): void {
|
|
const candidates = upgradePreview(sourceId)
|
|
if (candidates.length === 0) {
|
|
toast('Nothing to upgrade — every downloaded video is already at the target quality.', 'info')
|
|
return
|
|
}
|
|
const n = upgradeItems(sourceId, candidates)
|
|
toast(`Re-queued ${n} video${n === 1 ? '' : 's'} to upgrade quality.`, 'info')
|
|
}
|
|
|
|
return (
|
|
<div className={styles.root}>
|
|
<Caption1 className={styles.heading}>Retention & quality</Caption1>
|
|
{confirmIds ? (
|
|
<div className={styles.controls}>
|
|
<Caption1 className={styles.muted}>
|
|
Delete {confirmIds.length} older downloaded file{confirmIds.length === 1 ? '' : 's'}?
|
|
The videos stay indexed and won't re-download.
|
|
</Caption1>
|
|
<div className={styles.spacer} />
|
|
<Button
|
|
size="small"
|
|
appearance="primary"
|
|
icon={<DeleteRegular />}
|
|
onClick={onConfirmPrune}
|
|
>
|
|
Delete {confirmIds.length}
|
|
</Button>
|
|
<Button size="small" appearance="subtle" onClick={() => setConfirmIds(null)}>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className={styles.controls}>
|
|
<Field label="Keep newest">
|
|
<SpinButton
|
|
className={styles.narrow}
|
|
value={maxCount}
|
|
min={1}
|
|
max={9999}
|
|
onChange={(_, d) => {
|
|
const v = d.value ?? Number(d.displayValue)
|
|
if (typeof v === 'number' && Number.isFinite(v))
|
|
setMaxCount(Math.max(1, Math.round(v)))
|
|
}}
|
|
/>
|
|
</Field>
|
|
<Button size="small" appearance="subtle" icon={<DeleteRegular />} onClick={onPrune}>
|
|
Prune older files
|
|
</Button>
|
|
<div className={styles.spacer} />
|
|
<Button size="small" appearance="subtle" icon={<ArrowUpRegular />} onClick={onUpgrade}>
|
|
Upgrade below-target
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|