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:
+3
-3
@@ -410,9 +410,9 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
already that union).
|
already that union).
|
||||||
- [ ] **L16 — `relTime`/`formatWhen` verbosity.** `relTime` returns unbounded "N d ago"; History's
|
- [ ] **L16 — `relTime`/`formatWhen` verbosity.** `relTime` returns unbounded "N d ago"; History's
|
||||||
`formatWhen` always appends the year, even for the current year.
|
`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).
|
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.
|
view; consider hiding/disabling it.
|
||||||
- [ ] **L19 — `App.tsx` focus hack** — `setTimeout(() => …focus(), 60)` after a tab switch.
|
- [ ] **L19 — `App.tsx` focus hack** — `setTimeout(() => …focus(), 60)` after a tab switch.
|
||||||
- [ ] **L20 — `youtubePlayerClient` is free text** with no allowlist/validation; a typo passes
|
- [ ] **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] **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
|
- [x] **L55 — QueueItem meta line can show size twice** — a probed-format `quality` label already
|
||||||
contains the size, then `sizeLabel` is appended again.
|
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.
|
`length > 0`) with no UI warning.
|
||||||
- [x] **L57 — Scheduling a past datetime silently downloads now** (`buildItem` future-only guard),
|
- [x] **L57 — Scheduling a past datetime silently downloads now** (`buildItem` future-only guard),
|
||||||
no feedback that the schedule was ignored.
|
no feedback that the schedule was ignored.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function App(): React.JSX.Element {
|
|||||||
const theme = useSettings((s) => s.theme)
|
const theme = useSettings((s) => s.theme)
|
||||||
const accentColor = useSettings((s) => s.accentColor)
|
const accentColor = useSettings((s) => s.accentColor)
|
||||||
const updateSettings = useSettings((s) => s.update)
|
const updateSettings = useSettings((s) => s.update)
|
||||||
|
const showTerminal = useSettings((s) => s.customCommandEnabled)
|
||||||
const isDark = useResolvedDark()
|
const isDark = useResolvedDark()
|
||||||
const [tab, setTab] = useState<TabValue>('downloads')
|
const [tab, setTab] = useState<TabValue>('downloads')
|
||||||
const [paletteOpen, setPaletteOpen] = useState(false)
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
||||||
@@ -171,6 +172,7 @@ function App(): React.JSX.Element {
|
|||||||
version={version}
|
version={version}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
onToggleCollapsed={toggleCollapsed}
|
onToggleCollapsed={toggleCollapsed}
|
||||||
|
showTerminal={showTerminal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className={styles.content}>
|
<main className={styles.content}>
|
||||||
|
|||||||
@@ -196,7 +196,15 @@ export function DownloadOptionsForm({ value, onChange }: Props): React.JSX.Eleme
|
|||||||
onChange={(v) => setOpt('sponsorBlockMode', v as SponsorBlockMode)}
|
onChange={(v) => setOpt('sponsorBlockMode', v as SponsorBlockMode)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</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}>
|
<div className={styles.categoryGrid}>
|
||||||
{SPONSORBLOCK_CATEGORIES.map((cat) => (
|
{SPONSORBLOCK_CATEGORIES.map((cat) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ interface SidebarProps {
|
|||||||
version: string
|
version: string
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
onToggleCollapsed: () => void
|
onToggleCollapsed: () => void
|
||||||
|
/** Show the Terminal nav item (only when custom commands are enabled) */
|
||||||
|
showTerminal: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
@@ -162,7 +164,8 @@ export function Sidebar({
|
|||||||
onSetTheme,
|
onSetTheme,
|
||||||
version,
|
version,
|
||||||
collapsed,
|
collapsed,
|
||||||
onToggleCollapsed
|
onToggleCollapsed,
|
||||||
|
showTerminal
|
||||||
}: SidebarProps): React.JSX.Element {
|
}: SidebarProps): React.JSX.Element {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const focus = useFocusStyles()
|
const focus = useFocusStyles()
|
||||||
@@ -214,7 +217,7 @@ export function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.nav}>
|
<div className={styles.nav}>
|
||||||
{NAV.map((n) => {
|
{NAV.filter((n) => n.value !== 'terminal' || showTerminal).map((n) => {
|
||||||
const active = tab === n.value
|
const active = tab === n.value
|
||||||
const btn = (
|
const btn = (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Field,
|
||||||
Input,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
Caption1,
|
Caption1,
|
||||||
@@ -103,10 +104,21 @@ export function TemplateManager(): React.JSX.Element {
|
|||||||
setDraft({ id: t.id, name: t.name, args: t.args, urlPattern: t.urlPattern ?? '' })
|
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 {
|
function commit(): void {
|
||||||
if (!draft) return
|
if (!draft) return
|
||||||
const name = draft.name.trim()
|
const name = draft.name.trim()
|
||||||
if (!name) return
|
if (!name || patternError()) return
|
||||||
const urlPattern = draft.urlPattern.trim()
|
const urlPattern = draft.urlPattern.trim()
|
||||||
save({
|
save({
|
||||||
id: draft.id ?? newId('tpl'),
|
id: draft.id ?? newId('tpl'),
|
||||||
@@ -165,11 +177,16 @@ export function TemplateManager(): React.JSX.Element {
|
|||||||
resize="vertical"
|
resize="vertical"
|
||||||
onChange={(_, d) => setDraft({ ...draft, args: d.value })}
|
onChange={(_, d) => setDraft({ ...draft, args: d.value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Field
|
||||||
value={draft.urlPattern}
|
validationState={patternError() ? 'error' : 'none'}
|
||||||
placeholder="Auto-apply to URLs matching (regex, optional) — e.g. soundcloud\.com"
|
validationMessage={patternError()}
|
||||||
onChange={(_, d) => setDraft({ ...draft, urlPattern: d.value })}
|
>
|
||||||
/>
|
<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}>
|
<div className={styles.formActions}>
|
||||||
<Button appearance="subtle" icon={<DismissRegular />} onClick={() => setDraft(null)}>
|
<Button appearance="subtle" icon={<DismissRegular />} onClick={() => setDraft(null)}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
Reference in New Issue
Block a user