mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-25 10:07:44 +00:00
feat(torrent): complete lifecycle controls and diagnostics
- add durable Torrent telemetry, availability, sharing, relocation, and web-seed workflows\n- fence queue ownership, lifecycle recovery, persistence, and native control races\n- add localized UI, generated bindings, regression coverage, and smoke validation
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import type { TorrentFile } from '../bindings/TorrentFile';
|
||||
import type { TorrentFileSelectionEntry } from '../bindings/TorrentFileSelectionEntry';
|
||||
import { normalizeTorrentWebSeedDrafts, type TorrentWebSeedDraft } from '../utils/downloads';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type TorrentWebSeedFile = Pick<TorrentFile, 'index' | 'path'> | Pick<TorrentFileSelectionEntry, 'index' | 'relativePath'>;
|
||||
|
||||
type Props = {
|
||||
files: readonly TorrentWebSeedFile[];
|
||||
rows: readonly TorrentWebSeedDraft[];
|
||||
onChange: (rows: TorrentWebSeedDraft[]) => void;
|
||||
disabled?: boolean;
|
||||
idPrefix: string;
|
||||
};
|
||||
|
||||
const filePath = (file: TorrentWebSeedFile): string => 'path' in file ? file.path : file.relativePath;
|
||||
|
||||
export const TorrentWebSeedEditor = ({ files, rows, onChange, disabled = false, idPrefix }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const uriRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
const focusAfterRemoveRef = useRef<number | null>(null);
|
||||
const filesForValidation = files.map(file => ({ index: file.index }));
|
||||
const rowsAreValid = normalizeTorrentWebSeedDrafts(rows, filesForValidation) !== null;
|
||||
useEffect(() => {
|
||||
const rowIndex = focusAfterRemoveRef.current;
|
||||
focusAfterRemoveRef.current = null;
|
||||
if (rowIndex !== null) uriRefs.current[rowIndex]?.focus();
|
||||
}, [rows]);
|
||||
const addRow = () => onChange([...rows, { fileIndex: files[0]?.index ?? null, uri: '' }]);
|
||||
const updateRow = (rowIndex: number, update: Partial<TorrentWebSeedDraft>) => onChange(
|
||||
rows.map((row, index) => index === rowIndex ? { ...row, ...update } : row)
|
||||
);
|
||||
const removeRow = (rowIndex: number) => {
|
||||
const nextRows = rows.filter((_, index) => index !== rowIndex);
|
||||
focusAfterRemoveRef.current = nextRows.length > 0 ? Math.min(rowIndex, nextRows.length - 1) : null;
|
||||
onChange(nextRows);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{rows.length === 0 && (
|
||||
<p className="text-[11px] text-text-muted">{t($ => $.properties.torrentWebSeedsEmpty)}</p>
|
||||
)}
|
||||
{rows.map((row, rowIndex) => {
|
||||
const rowId = `${idPrefix}-${rowIndex}`;
|
||||
const rowIsValid = normalizeTorrentWebSeedDrafts([row], filesForValidation) !== null;
|
||||
return (
|
||||
<div key={rowId} className="grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)_auto] gap-2 items-end">
|
||||
<div className="min-w-0">
|
||||
<label htmlFor={`${rowId}-file`} className="block text-[10px] text-text-muted mb-1">
|
||||
{t($ => $.properties.torrentWebSeedsFile)}
|
||||
</label>
|
||||
{files.length === 1 ? (
|
||||
<select
|
||||
id={`${rowId}-file`}
|
||||
value={files[0].index}
|
||||
onChange={() => undefined}
|
||||
disabled
|
||||
aria-invalid={!rowIsValid}
|
||||
className="app-control w-full min-h-[30px] px-2 py-1.5 text-xs disabled:opacity-70"
|
||||
>
|
||||
<option value={files[0].index}>{files[0].index + 1}: {filePath(files[0])}</option>
|
||||
</select>
|
||||
) : (
|
||||
<select
|
||||
id={`${rowId}-file`}
|
||||
value={row.fileIndex ?? ''}
|
||||
onChange={event => updateRow(rowIndex, { fileIndex: Number(event.currentTarget.value) })}
|
||||
disabled={disabled || files.length === 0}
|
||||
aria-invalid={!rowIsValid}
|
||||
className="app-control w-full min-h-[30px] px-2 py-1.5 text-xs disabled:opacity-50"
|
||||
>
|
||||
<option value="" disabled>{t($ => $.properties.torrentWebSeedsFile)}</option>
|
||||
{files.map(file => (
|
||||
<option key={file.index} value={file.index}>
|
||||
{file.index + 1}: {filePath(file)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<label htmlFor={`${rowId}-uri`} className="block text-[10px] text-text-muted mb-1">
|
||||
{t($ => $.properties.torrentWebSeedsUri)}
|
||||
</label>
|
||||
<input
|
||||
id={`${rowId}-uri`}
|
||||
type="url"
|
||||
ref={element => { uriRefs.current[rowIndex] = element; }}
|
||||
value={row.uri}
|
||||
onChange={event => updateRow(rowIndex, { uri: event.currentTarget.value })}
|
||||
disabled={disabled}
|
||||
aria-invalid={!rowIsValid}
|
||||
placeholder="https://mirror.example/torrent/"
|
||||
className="app-control w-full min-h-[30px] px-2 py-1.5 text-xs font-mono disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRow(rowIndex)}
|
||||
disabled={disabled}
|
||||
aria-label={t($ => $.properties.torrentWebSeedsRemove)}
|
||||
className="app-button min-h-[30px] px-2 text-xs disabled:opacity-50"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{rows.length > 0 && !rowsAreValid && (
|
||||
<p className="text-[11px] text-red-500" role="alert">
|
||||
{t($ => $.properties.torrentWebSeedsInvalid)}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addRow}
|
||||
disabled={disabled || files.length === 0}
|
||||
className="app-button px-3 text-xs disabled:opacity-50"
|
||||
>
|
||||
+ {t($ => $.properties.torrentWebSeedsAdd)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user