1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

fix(frontend): QA fixes — playlist UX, selection stroke, URL expand

CaptureDialog.jsx:
- Placeholder no longer appears on idle playlist rows (only during
  probing); chevron shows only after probe completes — no left-padding
  while the user is still typing
- Per-video delete button added to expanded playlist list; removes item
  from playlistItems so its ID is absent from per_item_quality on submit
- anyEmptyPlaylist guard: Archive button disabled + handleArchive early-
  return when all videos have been deleted (empty map would otherwise
  silently download everything)

styles.css:
- Selection stroke switched from outline to box-shadow:inset everywhere
  (entry-row-outer, child-entry-row, legacy flat-div selector) —
  guaranteed inside element bounds, no layout interference
- URL cell overflow only on :hover; removed is-selected .url-cell rule
  that was expanding child URLs when their parent was selected
- Playlist items redesign: separator lines instead of background fills,
  amber left-border for conflicts, thin scrollbar, tighter padding
- Remove button: opacity 0.3 always-visible baseline; full opacity on
  hover/:focus-visible; forced to 1 on coarse-pointer (touch) devices
This commit is contained in:
TheGeneralist 2026-07-21 13:18:40 +02:00
parent e2f50a9856
commit f0a6bf1cdc
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
7 changed files with 123 additions and 78 deletions

View file

@ -395,6 +395,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
if (toSubmit.some(it => hasConflict(it))) return
if (toSubmit.some(it => it.probeState === 'probing' ||
(isPlaylistSource(it.locator) && it.playlistProbeState !== 'done'))) return
if (toSubmit.some(it => Array.isArray(it.playlistItems) && it.playlistItems.length === 0)) return
const batchId = toSubmit.length > 1
? (crypto.randomUUID?.() ?? `batch-${Date.now()}`)
: null
@ -516,9 +517,20 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
function updateSync(id, val) {
setItems(prev => prev.map(it => it.id === id ? { ...it, syncEnabled: val } : it))
}
function deletePlaylistItem(itemId, videoId) {
setItems(prev => prev.map(it =>
it.id !== itemId ? it :
{ ...it, playlistItems: it.playlistItems.filter(pi => pi.id !== videoId) }
))
}
const pendingCount = items.filter(it => it.locator.trim()).length
const anyConflict = items.some(it => hasConflict(it))
// True if any playlist row has had all its videos deleted archive would be a no-op.
const anyEmptyPlaylist = items.some(it =>
Array.isArray(it.playlistItems) && it.playlistItems.length === 0
)
const anyProbing = items.some(it =>
it.probeState === 'probing' ||
// For playlist sources block unless probe completed successfully:
@ -557,6 +569,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
onPlaylistItemQualityChange={(vid, q) => updatePlaylistItemQuality(item.id, vid, q)}
onPlaylistToggle={() => togglePlaylistExpanded(item.id)}
onSyncChange={val => updateSync(item.id, val)}
onPlaylistItemDelete={(vid) => deletePlaylistItem(item.id, vid)}
/>
))}
</div>
@ -680,7 +693,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
type="button"
className="capture-submit"
onClick={handleArchive}
disabled={pendingCount === 0 || anyConflict || anyProbing}
disabled={pendingCount === 0 || anyConflict || anyProbing || anyEmptyPlaylist}
>
{pendingCount > 1 ? `Archive ${pendingCount}` : 'Archive'}
</button>
@ -694,7 +707,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
}
function CaptureRow({ item, autoFocus, onLocatorChange, onQualityChange, onRemove, onSubmit,
onPlaylistQualityChange, onPlaylistItemQualityChange, onPlaylistToggle, onSyncChange }) {
onPlaylistQualityChange, onPlaylistItemQualityChange, onPlaylistToggle, onSyncChange, onPlaylistItemDelete }) {
const inputRef = useRef(null)
useEffect(() => {
@ -805,9 +818,9 @@ function CaptureRow({ item, autoFocus, onLocatorChange, onQualityChange, onRemov
>
{item.playlistExpanded ? '▲' : '▼'}
</button>
) : (
) : item.playlistProbeState === 'probing' ? (
<span className="capture-playlist-toggle-placeholder" aria-hidden="true" />
)
) : null
) : null}
<input
ref={inputRef}
@ -852,6 +865,14 @@ function CaptureRow({ item, autoFocus, onLocatorChange, onQualityChange, onRemov
{pi.quality === null && (
<span className="capture-playlist-conflict-badge">Choose quality</span>
)}
<button
type="button"
className="capture-playlist-item-remove"
aria-label={`Remove ${pi.title || pi.url}`}
onClick={() => onPlaylistItemDelete(pi.id)}
>
&times;
</button>
</div>
))}
{syncToggle}