Owns the shared single-files so the S6a (Local) / S6b (Remote) editor slices touch
disjoint files. Editor screens are stubbed (MediaSourceEditorPlaceholder) for S6.
- web/src/api/mediaSources.ts (+test): client module over the new media-source write
endpoints (local CRUD + move/path-exists; Plex pin-flow/sign-out; shared remote
state/connection/libraries/path-replacements/refresh; family = only URL variance),
DTOs re-exported from generated v1, messageFromMediaSourcesError; barrel export.
- web/src/mediaSources/{familyMeta,paths,pinFlowPoll}.ts (+tests): family labels/routes/
remote-path column naming (owns RemoteFamily); client-side NormalizePath mirror for
in-draft dup detection; pure §C1 pin-flow poll state machine (waiting/finalizing/
success/timeout/budget-exhausted), timer-free and fully unit-tested.
- routing.ts parseLibrariesSubRoute + LibrariesSubRoute union split Local vs Remote.
- App.tsx: libraries route allowSubPaths; LibrariesRouteScreen wrapper dispatching a
flat switch to placeholders; App-owned popstate (finding 4) — App is the single
popstate owner, consults canLeaveCurrentScreen() and only on approval updates
librariesSubPath passed DOWN to the wrapper (wrapper never self-listens); state write
scoped to the libraries route so Playouts/Media pops stay byte-identical (nit 3).
- LibrariesScreen hub wiring: Add-Source menu (Local/Plex/Jellyfin/Emby), remote source
gear -> family screen, local library row gear -> edit route; removed the disabled
Scan-All button + the deferred-sources card (§C7/§D.1).
- Tests: App-owned-popstate dirty-guard case (confirm false keeps URL+sub-screen; true
navigates); mediaSources client URL/verb mapping; pinFlowPoll transitions; familyMeta/
paths units; hub-wiring navigation.
- docs/spa-conventions.md §8 (resolved sub-path+dirty-guard caveat -> App-owned popstate)
+ §2 exemplar list (guarded-route exception).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
30 lines
1.6 KiB
TypeScript
30 lines
1.6 KiB
TypeScript
// Client-side path normalization for in-draft duplicate detection in the local-library editor.
|
|
//
|
|
// This mirrors the *comparison-relevant* part of the Blazor `UpdateLocalLibraryHandler.NormalizePath`
|
|
// (`Path.GetFullPath(new Uri(path).LocalPath).TrimEnd(sep, altSep).ToUpperInvariant()`), which the
|
|
// server uses to merge/dedupe library paths by normalized string (design §C4c). A browser cannot
|
|
// reproduce `Path.GetFullPath` (it resolves relative paths against the server CWD and collapses
|
|
// `.`/`..`) — that resolution stays authoritative on the server. What we CAN and must mirror for
|
|
// catching an obvious in-draft duplicate before a round-trip is the trailing-separator trim and the
|
|
// case-insensitive (upper-invariant) compare. Two already-absolute paths that differ only by a
|
|
// trailing slash or letter case are treated as the same path here, exactly as the server would.
|
|
|
|
/** Normalize a path for case/trailing-separator-insensitive comparison (browser-side subset). */
|
|
export function normalizePath(path: string): string {
|
|
return path.replace(/[\\/]+$/, '').toUpperCase();
|
|
}
|
|
|
|
/** True when `a` and `b` name the same path under {@link normalizePath}. */
|
|
export function pathsEqual(a: string, b: string): boolean {
|
|
return normalizePath(a) === normalizePath(b);
|
|
}
|
|
|
|
/**
|
|
* True when `candidate` already exists (by normalized string) in `existing`. Used to reject an
|
|
* Add-Path that duplicates a path already in the draft.
|
|
*/
|
|
export function isDuplicatePath(existing: readonly string[], candidate: string): boolean {
|
|
const normalizedCandidate = normalizePath(candidate);
|
|
return existing.some((path) => normalizePath(path) === normalizedCandidate);
|
|
}
|