Fix L17/L18/L56: regex validation, terminal visibility, SponsorBlock warning

L17: TemplateManager urlPattern field now validates the regex on each keystroke;
     an error message appears below the input and Save is blocked until fixed.
L18: Terminal nav item is hidden when customCommandEnabled is false -- no more
     dead-end gated view. Sidebar gains showTerminal prop; App.tsx reads the
     setting and passes it through.
L56: SponsorBlock Categories Field shows a 'warning' validationMessage when the
     toggle is ON but no categories are selected, making the silent no-op visible.

typecheck + 242 tests + eslint + prettier green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 13:48:41 -04:00
parent ba208de98b
commit 910cf23e61
5 changed files with 42 additions and 12 deletions
+3 -3
View File
@@ -410,9 +410,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
already that union).
- [ ] **L16 — `relTime`/`formatWhen` verbosity.** `relTime` returns unbounded "N d ago"; History's
`formatWhen` always appends the year, even for the current year.
- [ ] **L17 — TemplateManager regex not validated.** An invalid `urlPattern` is accepted with no
- [x] **L17 — TemplateManager regex not validated.** An invalid `urlPattern` is accepted with no
feedback and silently never matches (main's `matchesUrl` swallows the error).
- [ ] **L18 — Terminal nav item shown when custom commands are off** — leads to a gated dead-end
- [x] **L18 — Terminal nav item shown when custom commands are off** — leads to a gated dead-end
view; consider hiding/disabling it.
- [ ] **L19 — `App.tsx` focus hack**`setTimeout(() => …focus(), 60)` after a tab switch.
- [ ] **L20 — `youtubePlayerClient` is free text** with no allowlist/validation; a typo passes
@@ -486,7 +486,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L54 — DownloadBar preview `<img>` has no `onError` fallback** (inconsistent with `MediaThumb`'s retry).
- [x] **L55 — QueueItem meta line can show size twice** — a probed-format `quality` label already
contains the size, then `sizeLabel` is appended again.
- [ ] **L56 — SponsorBlock ON with zero categories silently no-ops** (`buildArgs` guards on
- [x] **L56 — SponsorBlock ON with zero categories silently no-ops** (`buildArgs` guards on
`length > 0`) with no UI warning.
- [x] **L57 — Scheduling a past datetime silently downloads now** (`buildItem` future-only guard),
no feedback that the schedule was ignored.
+2
View File
@@ -38,6 +38,7 @@ function App(): React.JSX.Element {
const theme = useSettings((s) => s.theme)
const accentColor = useSettings((s) => s.accentColor)
const updateSettings = useSettings((s) => s.update)
const showTerminal = useSettings((s) => s.customCommandEnabled)
const isDark = useResolvedDark()
const [tab, setTab] = useState<TabValue>('downloads')
const [paletteOpen, setPaletteOpen] = useState(false)
@@ -171,6 +172,7 @@ function App(): React.JSX.Element {
version={version}
collapsed={collapsed}
onToggleCollapsed={toggleCollapsed}
showTerminal={showTerminal}
/>
<main className={styles.content}>
@@ -196,7 +196,15 @@ export function DownloadOptionsForm({ value, onChange }: Props): React.JSX.Eleme
onChange={(v) => setOpt('sponsorBlockMode', v as SponsorBlockMode)}
/>
</Field>
<Field label="Categories">
<Field
label="Categories"
validationState={value.sponsorBlockCategories.length === 0 ? 'warning' : 'none'}
validationMessage={
value.sponsorBlockCategories.length === 0
? 'No categories selected — no segments will be skipped.'
: undefined
}
>
<div className={styles.categoryGrid}>
{SPONSORBLOCK_CATEGORIES.map((cat) => (
<Checkbox
+5 -2
View File
@@ -152,6 +152,8 @@ interface SidebarProps {
version: string
collapsed: boolean
onToggleCollapsed: () => void
/** Show the Terminal nav item (only when custom commands are enabled) */
showTerminal: boolean
}
export function Sidebar({
@@ -162,7 +164,8 @@ export function Sidebar({
onSetTheme,
version,
collapsed,
onToggleCollapsed
onToggleCollapsed,
showTerminal
}: SidebarProps): React.JSX.Element {
const styles = useStyles()
const focus = useFocusStyles()
@@ -214,7 +217,7 @@ export function Sidebar({
</div>
<div className={styles.nav}>
{NAV.map((n) => {
{NAV.filter((n) => n.value !== 'terminal' || showTerminal).map((n) => {
const active = tab === n.value
const btn = (
<button
@@ -1,6 +1,7 @@
import { useState } from 'react'
import {
Button,
Field,
Input,
Textarea,
Caption1,
@@ -103,10 +104,21 @@ export function TemplateManager(): React.JSX.Element {
setDraft({ id: t.id, name: t.name, args: t.args, urlPattern: t.urlPattern ?? '' })
}
function patternError(): string | undefined {
const p = draft?.urlPattern.trim()
if (!p) return undefined
try {
new RegExp(p)
return undefined
} catch {
return 'Invalid regex — this pattern will never match.'
}
}
function commit(): void {
if (!draft) return
const name = draft.name.trim()
if (!name) return
if (!name || patternError()) return
const urlPattern = draft.urlPattern.trim()
save({
id: draft.id ?? newId('tpl'),
@@ -165,11 +177,16 @@ export function TemplateManager(): React.JSX.Element {
resize="vertical"
onChange={(_, d) => setDraft({ ...draft, args: d.value })}
/>
<Field
validationState={patternError() ? 'error' : 'none'}
validationMessage={patternError()}
>
<Input
value={draft.urlPattern}
placeholder="Auto-apply to URLs matching (regex, optional) — e.g. soundcloud\.com"
onChange={(_, d) => setDraft({ ...draft, urlPattern: d.value })}
/>
</Field>
<div className={styles.formActions}>
<Button appearance="subtle" icon={<DismissRegular />} onClick={() => setDraft(null)}>
Cancel