Consolidate native apps into one monorepo - #1
Merged
Conversation
Lightweight replacement for Logi Options+ with HID++ protocol support, gesture recognition via CGEventTap, and macOS Tahoe-compatible action dispatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove archive/ directory (dead SPM-based code, superseded by Xcode project) - Fix hardcoded log path — now writes to ~/Library/Logs/MrMouse/ - Add MIT license - Add CONTRIBUTING.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fresh SwiftUI tvOS app scaffold ("Jelly TV") plus softplan.md capturing
the research synthesis and phased plan for a native tvOS Jellyfin client
targeting Apple TV 4K on tvOS 18. Plan covers the architecture decisions
(AVPlayerViewController, hand-rolled JellyfinClient actor, SwiftData,
Nuke, Keychain), the DeviceProfile shape, tvOS-specific gotchas, and a
"must not regress" list drawn from current SwiftFin tvOS 1.0.1 bugs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Outer dir, project bundle, inner source dir, and the @main struct all move off the literal "Jelly TV" name to eliminate shell-quoting friction on every xcodebuild/grep/find command. Display name "Jelly TV" is preserved via INFOPLIST_KEY_CFBundleDisplayName so the on-screen label doesn't change. PRODUCT_BUNDLE_IDENTIFIER stays com.cursorkittens.Jelly-TV to avoid signing/provisioning churn. Also bumps SWIFT_VERSION 5.0 → 6.0 in both Debug and Release so the app target compiles under the same Swift 6 strict-concurrency mode the upcoming SPM packages will use, avoiding cross-boundary Sendable warnings at the import site. Adds .motif/ to .gitignore for the active workflow state directory. Verified with xcodebuild against Apple TV 4K (3rd generation) Simulator, tvOS 26.2 — BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds JellyTV/Info.plist with NSLocalNetworkUsageDescription, NSAppTransportSecurity.NSAllowsLocalNetworking=YES (allows plain HTTP to RFC1918/.local addresses, sufficient for v1 LAN-only scope), and NSBonjourServices=[_jellyfin._tcp] for future mDNS discovery. Kept GENERATE_INFOPLIST_FILE=YES so Xcode still synthesizes CFBundleDisplayName, CFBundleIdentifier, etc. from build settings — INFOPLIST_FILE merges as the base layer. Plist lives at JellyTV/Info.plist (next to .xcodeproj, outside the synchronized source group) so the PBXFileSystemSynchronizedRootGroup doesn't double-process it as a resource and an info plist. Verified the built app's merged Info.plist contains all expected keys. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Creates Packages/{JellyfinAPI,Persistence,DesignSystem,Library,Player,Settings}
as sibling local packages. Each is swift-tools-version 6.2 with
.tvOS(.v26) platform, a single library target, and a placeholder
namespace enum so the module compiles. Library depends on
JellyfinAPI+DesignSystem; Settings depends on JellyfinAPI+Persistence+
DesignSystem. Other dependencies will be wired in their phases.
All 6 packages compile independently via swift build with cross-package
local path resolution working correctly. Not yet linked to the Xcode app
target — that's the human-in-the-loop step in Task 0.4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User added all 6 packages via Xcode (File → Add Package Dependencies → Add Local). Resulting pbxproj has the canonical objectVersion 77 form: XCLocalSwiftPackageReference entries with relativePath = ../Packages/<Name> (SRCROOT-relative since the project lives at JellyTV/JellyTV.xcodeproj), XCSwiftPackageProductDependency entries, and Frameworks build phase references. Verified by adding temporary import statements for JellyfinAPI, Persistence, Settings, DesignSystem to JellyTVApp.swift — full xcodebuild succeeded — then reverted. Real wiring lands in Phase 1.5. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Raw Security framework (SecItemAdd/Update/CopyMatching/Delete) with kSecAttrAccessibleAfterFirstUnlock (correct for tvOS — no lock screen). ~120 LOC, zero external dependencies. Replaces the KeychainAccess SPM dep softplan.md originally proposed (last released March 2021, maintenance concern). CredentialsStore exposes serverURL/accessToken/deviceId as throwing funcs (since deviceId lazily generates a UUID on first access and the persist step can throw). Service name is injectable so tests use a unique UUID-based service per test for isolation. Tests use Swift Testing (@test). Round-trip set/get/delete, overwrite, idempotent delete, missing-key nil, deviceId persistence, clear-wipes-all. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DTOs match the verified OpenAPI 10.11.8 spec — camelCase Swift properties with explicit CodingKeys mapping to PascalCase wire keys (locked decision per critic C10). UserDto includes hasPassword and primaryImageTag from the start to avoid Phase 2 schema churn (critic M4). QuickConnectResult.dateAdded is Date? with iso8601 decoding (M3). AuthenticationRequest uses "Pw" not "Password" — protected by an explicit test that asserts the encoded JSON. JellyfinError has dedicated cases for unauthenticated, quickConnectDisabled, and quickConnectExpired so the actor + SignInModel can do per-callsite remapping (critics B1, C4). JellyfinClientAPI is a Sendable protocol the actor will conform to, enabling SignInModel to be tested with a mock (critic C6). Phase 1.3 implements the actor. Tests use Swift Testing (@test) with golden JSON literals — protects against silent wire-format drift. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Actor conforming to JellyfinClientAPI. Owns URLSession(.ephemeral) (critic C11), serverURL/accessToken state, immutable deviceId/client metadata. Per-request URLRequest with the comma-separated MediaBrowser auth header (URL-percent-encoded values, critic C5), Token field omitted when nil. Generic send<T> maps 401 → .unauthenticated; per-callsite remapping in quickConnectInitiate (.unauthenticated → .quickConnectDisabled, critic C4) and quickConnectStatus (http 404 → .quickConnectExpired, critic B1). Tests use StubURLProtocol to intercept every URLRequest. Headline assertions: exact auth header format with and without token, percent encoding of special chars, "Pw" field in AuthenticateByName body, quickConnectInitiate 401 → quickConnectDisabled, quickConnectStatus 404 → quickConnectExpired, currentUser 401 → unauthenticated (proves per-callsite remap doesn't leak), bare-bool decode for /QuickConnect/Enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full state machine for the connect → choose mode → Quick Connect or password → signed in flow. SignInModel is @observable @mainactor and takes any JellyfinClientAPI + CredentialsStore so tests can use a mock. Per critic findings: - B2: normalizeServerURL prepends http:// if no scheme, requires non-nil host. Bare "192.168.1.50:8096" works. - B1: pollQuickConnectLoop catches quickConnectExpired (404 → expired secret), transitions to failed(.quickConnectExpired). No infinite spin. - M5: persist credentials FIRST in finishSignIn, then transition to signedIn. On persist failure, transition to failed(.persistFailed). - M6: try await Task.sleep (not try?) so CancellationError propagates; caught explicitly in the loop. - C8 in spirit: network errors → failed(.serverUnreachable), not silent retry. SignInView is a switch over the state with five subviews. Quick Connect shows the 6-char code at 96pt monospaced. Focus management via @focusstate. Pixel polish deferred to Phase 5. Tests use a MockJellyfinClient + per-test unique-service CredentialsStore. Cover: state initialization, URL normalization (3 cases), connect success, connect with invalid URL, connect with network error, QC disabled, QC expired (via direct pollQuickConnectLoop call with 10ms poll interval), password sign-in success (verifies token + URL persist), password sign-in wrong credentials. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SessionStore (@observable @mainactor in Settings package) is the canonical owner of the live JellyfinClient + CredentialsStore. Per critic C7, it lives at the app root and is shared with SignInModel via .client + .credentials so the same actor instance and Keychain backing store are used end-to-end. restore() distinguishes (per critic C8): - no credentials → signedOut - 401 unauthenticated → clear creds, signedOut - network error → reconnecting (creds preserved) - 5xx HTTP → reconnecting (creds preserved) - success → signedIn(user) This means a momentary network blip on launch never signs the user out. Reconnecting state offers retry + sign-out buttons. JellyTVApp instantiates SessionStore via a @State closure-initializer factory that builds a fresh JellyfinClient with the persistent deviceId. RootView switches over phase, and the SignInFlowView wrapper bridges SignInModel.state == .signedIn into sessionStore.didSignIn(user:) via .onChange. Tests (8 new, on top of the 11 from Phase 1.4 = 19 total in Settings): no creds → signedOut, valid creds → signedIn (verifies client configured), 401 → signedOut + creds cleared, network error → reconnecting + creds NOT cleared, 503 → reconnecting + creds NOT cleared, didSignIn → signedIn, signOut → signedOut + creds cleared + logout called. The "creds NOT cleared" assertion is the protection against critic C8 — this is the regression test for #1657 persistent login behavior. ContentView.swift deleted; JellyTVApp now hosts RootView directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds docs/manual-smoke.md covering the Phase 1 acceptance criteria a human needs to run on a real Jellyfin server: connect with/without URL scheme, Quick Connect happy path, Quick Connect expiry, Quick Connect disabled (per-callsite 401 remap regression test for critic C4), password sign-in success/failure, session persistence (#1657 regression test), and network-blip-on-launch behavior (critic C8 regression test). Phase 0/1 build verification: all 48 unit tests pass (8 Persistence, 21 JellyfinAPI, 19 Settings) and xcodebuild build is clean against Apple TV 4K (3rd generation) tvOS 26.2 simulator with zero warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validator caught dead code in JellyfinClient.sendIgnoringResponse: the body was discarded with `_` on the URLSession.data tuple, then the error path tried to decode ProblemDetails from a fabricated empty Data() via a meaningless `as? HTTPURLResponse == nil ? Data() : Data()` ternary that always returned empty. Capture the body and decode from it like send<T> does. Only affected logout() in practice (the sole sendIgnoringResponse callsite) and only on non-2xx, non-401 responses, but the code was clearly wrong. Tests + xcodebuild still pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add BaseItemDto (canonical Jellyfin item type) with essential fields - Add UserItemDataDto (playback position, played status, etc.) - Add userViews(), resumeItems(), nextUp(), latestItems() endpoints to JellyfinClientAPI + JellyfinClient - Add currentServerURL() to protocol for HomeModel to get server URL - Update MockJellyfinClient with new stub fields Implements softplan §6 Phase 2 endpoints.
- Add Nuke + NukeUI dependencies for image caching - Add JellyfinImage helper for constructing image URLs from item metadata - Add PosterCard component (focusable button with poster image) - Add Shelf component (horizontal scrollable row with title + cards) - Add HeroSection component (full-width backdrop with gradient overlay + play button) All components follow softplan §4 UI patterns: .borderless button style, .focusSection(), .scrollClipDisabled(), containerRelativeFrame.
- Add HomeModel with state machine (loading/loaded/failed) - Uses async let for parallel fetching of libraries, resume, nextUp - Fetches latest per library sequentially (needs parentId) - Add HomeView with ScrollView + LazyVStack + hero + shelves - Uses .scrollClipDisabled(), .scrollTargetBehavior(.viewAligned) - Add HomeModelTests covering success, network error, no server URL, unauthorized Wires into RootView.SignedInRootView replacing the Phase 1 placeholder.
- Replace SignedInRootView placeholder with HomeView(model:) - Add Library import to RootView - Update macOS platform versions to v15 for all packages (Nuke requirement) - Add JellyfinAPI dependency to Settings test target for MockJellyfinClient access These changes complete Phase 2 integration.
- HomeModel: drop type=="Folder"||"CollectionFolder" filter; /UserViews already returns the library list. Latest-per-library now populates. - HomeModel: parallelize latestItems with withThrowingTaskGroup. - HomeView: add HomeContent.isEmpty + empty-state view with Reload, so a user with no resume/nextup/latest content sees a focusable fallback instead of a blank screen. - DesignSystem: rewrite PosterCard for tvOS — drop bad containerRelativeFrame/opacity, switch to .buttonStyle(.card) (gated on os(tvOS)) for proper focus/parallax. - DesignSystem: add FocusedHomeItemKey + .focusedHomeItem(...) plumbing through Shelf/PosterCard so the hero backdrop can crossfade to the currently-focused item. - Tests: add HomeModelScrollRegressionTests guarding against duplicate ids and large libraries (Swiftfin#1906 class).
Pivots JellyTV from a generic Jellyfin home/library client to a
Live-TV-first tvOS app. The user's server has only Live TV (HDHomeRun
emulator + XMLTV with real EPG).
JellyfinAPI
- New DTOs: LiveTvChannel, LiveTvProgram, plus query-result wrappers.
Per-endpoint narrow DTOs (not extending BaseItemDto).
- New methods on JellyfinClientAPI + JellyfinClient:
liveTvChannels()
liveTvPrograms(channelIds:minStartDate:maxStartDate:)
/LiveTv/Programs uses repeated channelIds query items, ISO8601 date
params, enableTotalRecordCount=false, limit=2000.
- Tests: 7 LiveTV decoding tests (incl. .NET 7-digit fractional second
StartDate verification — confirms .iso8601 strategy already handles
this format) + 8 URLProtocol stub tests for the new client methods.
- Dedicated LiveTvStubURLProtocol class to avoid the shared static
handler racing across parallel suites.
LiveTV (new package)
- GuideModel: @mainactor @observable, .loading | .loaded | .failed,
fetches channels then programs in single window call. Drops past
programs and orphan channelIds. Widens minStartDate by 4h to catch
in-progress programs at the window edge. Injectable now() for tests.
- GuideContent: value type with helper accessors.
- GuideLayout: pixelsPerMinute=8, channel column 240pt, row 100pt.
- GuideView: vertical ScrollView -> HStack of (channel column outside
horizontal scroll, sticky horizontally; horizontal ScrollView with
TimeHeader + LazyHStack rows). NOW line as a TimelineView overlay
sibling to the grid so timeline ticks don't rebuild program cells
(focus survives ticks).
- ProgramCell + ChannelRow + TimeHeader + ChannelLabel.
- 7 GuideModel tests covering load success, empty channels, network
error, unauthorized, not configured, past programs dropped, orphan
channels ignored.
App target
- RootView signed-in path now uses GuideView(model: GuideModel(...)).
- import Library + unused import DesignSystem removed.
- LiveTV package added to JellyTV target.
Existing fakes (HomeModelTests, HomeModelScrollRegressionTests,
Settings/MockJellyfinClient) updated with stub implementations of the
new protocol methods. All 76 existing tests still pass.
Plan / critic / validation went through /motif:dev. Two critics flagged:
- Date decoder rewrite is unnecessary churn (.iso8601 handles 7-frac
seconds) — dropped.
- maxEndDate vs maxStartDate naming — fixed.
- onScrollGeometryChange sticky overlay risky on tvOS — replaced with
nested-scroll layout.
- LazyVStack + eager HStack would materialize ~8000 buttons —
switched per-row HStack to LazyHStack.
- TimelineView wrapping the whole grid would drop focus every minute
— restructured to wrap NOW line only.
Wires the EPG guide to actually play channels via AVPlayer. ChannelLabel
becomes the focusable Button (per UX choice — only channels are
selectable, not programs); ProgramCell loses its Button wrapper and is
now a non-focusable visual rectangle with a RoundedRectangle background.
fullScreenCover (tvOS) / sheet (macOS) presents LiveTVPlayerView when a
channel is selected.
JellyfinAPI
- New types: LiveStreamPlayback (URL + liveStreamId), MediaSourceInfo +
LiveStreamResponse (decodes both singular MediaSource and plural
MediaSources shapes), LiveStreamOpenRequest (kept but unused),
PlaybackInfoBody, DeviceProfileBody + DirectPlay/Transcoding nested
body types.
- New JellyfinClientAPI method liveTvOpenStream(channelId:) implemented
by JellyfinClient using POST /Items/{itemId}/PlaybackInfo with
autoOpenLiveStream=true (the canonical Swiftfin/web-client path —
/LiveStreams/Open returned 500 from this server).
- Lazy userId caching on the actor (cleared on setAccessToken). Required
by PlaybackInfo's userId query param.
- Safe playback URL construction in makePlaybackURL: prefers the
server-supplied transcodingUrl resolved via URL(string:relativeTo:)
(never appendingPathComponent — that percent-encodes ?), falls back
to a URLComponents-built /Videos/{id}/stream.{container} URL with
api_key baked in. For live streams, rewrites /videos/{id}/stream →
/videos/{id}/master.m3u8 (HLS) since AVPlayer can't consume Jellyfin's
progressive-download endpoint for live media. Also strips empty
leading query items that Jellyfin sometimes emits (?&...).
- 7 new URLProtocol stub tests covering path, body shape, transcoding
URL rewrite + empty-param cleanup, direct-stream fallback, plural
MediaSources fallback, missing token, 401.
LiveTV package
- New LiveTVPlayerView wrapping AVPlayerViewController via
UIViewControllerRepresentable with strict #if os(tvOS) guards on
AVKit/UIKit imports + the representable conformance. Coordinator is
@mainactor; cleanup happens in dismantleUIViewController. KVO on
AVPlayerItem.status + observer on AVPlayerItemFailedToPlayToEndTime
surface playback errors via os.Logger.
- macOS fallback shows a "tvOS only" stub so the package compiles for
both platforms.
- ChannelPlayerPresentation ViewModifier picks fullScreenCover on tvOS,
sheet on macOS (fullScreenCover is unavailable on macOS).
- ProgramCell now non-focusable; added RoundedRectangle background to
preserve visual cell boundary that .buttonStyle(.card) used to give.
- ChannelLabel wraps content in a Button with .buttonStyle(.card) on
tvOS / .plain on macOS, calls onSelect(channel) closure.
- ChannelRow lost its onSelectProgram parameter; GuideView programArea
call site updated.
- GuideView gets @State var selectedChannel and passes the selection
through ChannelLabel's onSelect.
- GuideModel.openStream(channelId:) — public delegating method so the
player can resolve a stream URL without GuideView needing access to
the underlying client (token + actor stay encapsulated).
Logging
- New JellytvLog enum exposing per-category os.Logger instances under
subsystem tv.jelly.JellyTV (api / livetv / player / session).
- JellyfinClient.send now logs every request/response, decode failures
with a body snippet, HTTP errors with the ProblemDetails title/detail,
401 mapping, and network errors with URLError code.
- liveTvOpenStream logs channel id, resolved MediaSource fields, and
the final playback URL.
- GuideModel.load logs begin / loaded / each error path.
- LiveTVPlayerView logs stream-open lifecycle and AVPlayerItem status
changes (incl. .failed with the underlying AVError).
- JellyfinAPI Package.swift now declares .macOS(.v15) — was tvOS-only,
which broke os.Logger availability when SPM built the package for
swift test on the Mac.
Info.plist
- Added NSAllowsArbitraryLoadsForMedia. URLSession honors
NSAllowsLocalNetworking but AVFoundation does not — without this
exception AVPlayer would fail with the misleading
-11850 "Operation Stopped" / -12939 chain on cleartext LAN streams.
Existing fakes (HomeModelTests, HomeModelScrollRegressionTests,
Settings/MockJellyfinClient, LiveTV/FakeJellyfinClient) updated with
stub liveTvOpenStream implementations. FakeJellyfinClient additionally
exposes a Result<LiveStreamPlayback, Error> stub property and captures
lastOpenStreamChannelId for assertions.
All 83 tests across all packages pass (43 JellyfinAPI, 6 Library, 19
Settings, 7 LiveTV, 8 Persistence).
Plan / 2 critics / validation went through /motif:dev. Critics caught:
- LiveStreamResponse needs to handle both singular MediaSource and
plural MediaSources shapes (different Jellyfin versions)
- transcodingUrl resolution must use URL(string:relativeTo:), never
appendingPathComponent (would percent-encode the ?)
- Don't double-inject api_key when Jellyfin already baked it into
transcodingUrl
- accessToken nil must throw .unauthenticated, not silently produce
a URL with empty api_key=
- GuideModel.client should stay private; expose openStream delegating
method instead of leaking the client to the view layer
- import UIKit/AVKit must be inside #if os(tvOS), not just usage —
UIKit doesn't exist on macOS
- Tests must explicitly assert no double api_key and no percent-encoded ?
Known v1 limitations (deferred):
- /LiveStreams/Close not wired on dismiss; relies on Jellyfin's idle
timeout for cleanup. Acceptable since the HDHomeRun emulator allows
multiple concurrent streams.
- DeviceProfile is minimum-viable; if a channel won't play the player
view shows a Retry/Dismiss state and we'd iterate the profile.
- ProgramCell is now visually-only — the user explicitly chose
channel-only selection over program selection.
Native macOS live wallpaper app built for macOS 26 (Tahoe) with Liquid Glass. Features: - Multi-display video wallpaper playback via AVPlayerLayer-backed NSWindow - Curated catalog + local imports (MP4/MOV) - Liquid Glass chrome (buttons, tag chips, badges, row backgrounds) - Video preview on card hover (cached files only, pooled for perf) - Live video playback in detail view - First-launch welcome sheet - Now Playing section with per-display stop controls - Drag-and-drop import - Battery-aware auto-pause (via SettingsManager) - Menu bar extra for quick access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Features: - Unapply wallpapers per-display (engine.stop(forDisplay:)) with UI in Now Playing cards and detail-view Remove button - Card grid spacing bumped 16→22 and hover shadow/scale softened to prevent visual overlap under hover - Opaque tag-filter strip background to prevent scroll bleed-through Error handling & robustness: - New AppLogger (os.Logger namespace per subsystem) replacing every print() in Services with filterable, privacy-aware logging - New AppErrorPresenter: shared @mainactor presenter with title-dedup, single .appErrorAlert() modifier attached once per WindowGroup root, nonisolated static report() for service call sites - AVPlayerItem.status KVO in WallpaperPlayerView with stored observation token and cleanup in stop() — surfaces playback failures - Stale-file detection in WallpaperCatalog (staleLocalWallpaperIDs); WallpaperCardView shows subtle grayscale + "Missing" badge; WallpaperDetailView offers Remove Missing Entry - WallpaperEngine.ensureAvailable() stale precheck runs once in apply() and once in setWallpaper() so Apply-to-All surfaces a single error - WallpaperCatalog.removeLocalWallpaper() prunes array + deletes file - loadSeedCatalog split into explicit missing-file / read / decode steps - SettingsManager reverts launchAtLogin toggle on SMAppService failure - DownloadManager @mainactor (fixes background @published warning) - Import failures route through the global presenter; local importError state removed from ImportView - Drop-to-import error paths fully wired in GalleryView.handleDrop Docs: - README.md with features, requirements, architecture, log filters - CLAUDE.md with project conventions and gotchas (Liquid Glass rules, @ObservedObject vs @StateObject, os import requirement, NSResponder.presentError name clash, stale-file design, non-goals) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Six independent improvements landed in one pass: - DownloadManager: rewritten to use URLSessionDownloadDelegate so the download progress fraction is real, not indeterminate. WallpaperDetailView now binds ProgressView(value:) to the live fraction. - WallpaperPlayerView: KVO on AVPlayer.timeControlStatus surfaces silent stalls via AppErrorPresenter, gated on hasPlayedAtLeastOnce to suppress initial-buffer false positives, with a 60s rate-limit between reports. - AppErrorPresenter: NSAlert fallback when no titled host window is available, so errors fired while only the menu bar is visible aren't silently dropped (SwiftUI .alert can't propagate through MenuBarExtra). - AppDelegate: refreshStaleStatus now runs at launch and on every didBecomeActive notification, so returning to the app re-checks imported files. - Tools/seed-catalog.swift: standalone Swift script that hits the Pexels Videos API and writes catalog.generated.json (does not clobber the curated catalog.json). PEXELS_API_KEY via env var. - livewallTests target: first test target in the repo, using Swift Testing and PBXFileSystemSynchronizedRootGroup. 8 test cases covering AppErrorPresenter dedup, WallpaperCatalog stale detection, and DownloadManager progress publishing. ENABLE_TESTABILITY also added to the app's Release config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…technique Based on InstantSpaceSwitcher by jurplel (MIT license). This fixes desktop switching on macOS 26 Tahoe where CGS APIs are broken and CGEvent modifier flags are stripped by the WindowServer. The technique synthesizes a trackpad swipe gesture with high velocity (400.0) which triggers instant space switching bypassing broken APIs. - Added ISS.c/ISS.h with C implementation of gesture synthesis - Updated DesktopSwitcher.swift to use ISS functions - Added bridging header configuration in project.pbxproj - Updated README.md with the fix and credits
Introduce DesignSystem.swift (Metrics, Palette, StatusPill, ActiveBadge) so spacing, radii, the "active" green, and chrome pills are one source of truth. Share DisplayManager via .shared so Settings, Detail, Gallery, and the menu bar all stay reactive to the same instance. Fix per-view discrepancies: card preview letterbox, card metadata pill vs. glass rule, detail glass-on-content, unlabeled download progress, non-reactive settings displays, orphaned import tip, unstable menu bar ordering, welcome CTA floating outside its card, hero CTA duplication, pinned tag-strip backdrop, and the drop overlay's invisible accent tint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebuilds the Live TV experience around a TabView shell (On Now / Guide / Recordings) backed by the full Jellyfin Live TV API. Existing GuideModel and program/channel DTOs stay backward-compatible; everything new is purely additive. JellyfinAPI - Extend `LiveTvChannel` with `userData` (favorites) and inline `currentProgram` (`addCurrentProgram=true` shape). - Extend `LiveTvProgram` with channel context, genres, ratings, image tags, runtime — fields the UI needs for hero/detail/guide rendering. - New DTOs: `TimerInfoDto`, `SeriesTimerInfoDto`, `LiveTvChannelFilters`, `LiveTvProgramFilters`. - New `JellyfinClient` endpoints (with `JellyfinClientAPI` defaults so existing mocks keep compiling): - `liveTvChannels(filters:addCurrentProgram:)` (full filter surface) - `liveTvPrograms(channelIds:minStartDate:maxStartDate:filters:)` - `liveTvRecommendedPrograms` - `liveTvProgram(programId:)` - `liveTvRecordings(isInProgress:seriesTimerId:limit:)` - `deleteLiveTvRecording` - `liveTvTimers`, `liveTvSeriesTimers`, `liveTvTimerDefaults` - `createLiveTvTimer`, `createLiveTvSeriesTimer` - `cancelLiveTvTimer`, `cancelLiveTvSeriesTimer` - `setFavorite(itemId:isFavorite:)` DesignSystem - New `ChannelLogoView` with letter-bug fallback + `LiveBadge` pulser. - `LiveTvFormat` (time-range / progress) for shared EPG formatting. - Image URL helpers on `LiveTvChannel` (logo) and `LiveTvProgram` (tile, backdrop) with sensible tag fallbacks. LiveTV - `LiveTVRootView`: TabView shell holding On Now / Guide / Recordings, with centralized full-screen player + program-detail presentation. - `OnNowView`: Plex-style hero (focused channel/program drives backdrop) plus curated shelves — Favorites, On Now, Sports, Movies, News, Kids, Up Next, Recordings. - `GuideView` rebuilt: filter pill bar (All / Favorites / Movies / Sports / News / Kids), channel logos in the row header, focusable program cells with live-progress bar, and a sticky focused-program detail strip. - `ProgramDetailView`: backdrop, badges, channel chip, time/year/rating, overview, Watch / Record (toggle) / Close action row. - `RecordingsView`: Recording Now / Scheduled / Series / Recorded sections with timer cancel + recording delete. - `LiveTVPlayerView`: enhanced loading state (channel logo + name) and AVPlayerItem.externalMetadata injection (title, channel chip, overview, genre, year) so the tvOS press-up info panel reads beautifully. App - `RootView` now lands on a TabView with Home (Library) + Live TV. - Per-tab models held in `@State` so loaded content survives re-renders. Tests - New: `LiveTvTimerDecodingTests`, `JellyfinClientLiveTvExtendedTests` (channels-with-filters / recommended / recordings / timers / favorites paths and methods), `OnNowModelTests`, `RecordingsModelTests`. - Extended: `FakeJellyfinClient` (filtered channel + recommended + recordings + timer stubs), `GuideModelTests` (filter routing). https://claude.ai/code/session_01QyJHH7BXTF4WHagXNihV2k
…rewrite)
The Phase-C playback path silently rewrote progressive `/videos/{id}/stream`
URLs to `/videos/{id}/master.m3u8`, but the query params Jellyfin attached
were progressive-style (no SegmentContainer/MinSegments/BreakOnNonKeyFrames)
so the master.m3u8 endpoint couldn't serve a valid manifest — AVPlayer
errored with CoreMediaErrorDomain -16847 / NSURLErrorResourceUnavailable
-1008 every time.
Fix the upstream cause: send a `container=ts`, `protocol=hls`,
`BreakOnNonKeyFrames=true` transcoding profile (matching the Jellyfin web
client's live-TV profile) so the server returns a real HLS URL natively.
Then drop the client-side path rewrite — silent rewrites mask
profile/server mismatches, which is exactly how this bug shipped.
Validated end-to-end against a live Jellyfin server: master playlist,
variant playlist, and segment all return HTTP 200; segment is valid
MPEG-TS (sync byte 0x47); live TV plays on the simulator.
JellyfinAPI
- TranscodingProfileBody gains `breakOnNonKeyFrames: Bool` (PascalCase
CodingKey, no init default — consistent with the struct's all-required
style; only caller is `liveTvDefault`).
- `DeviceProfileBody.liveTvDefault` HLS transcode profile changed:
container `mp4`→`ts`, videoCodec `h264`→`h264,hevc` (server can
stream-copy hevc instead of re-encoding), audioCodec
`aac`→`aac,mp3,ac3,eac3`, minSegments `1`→`2`, `breakOnNonKeyFrames=true`.
DirectPlay profile unchanged.
- `JellyfinClient.makePlaybackURL`: removed `/stream`→`/master.m3u8`
path rewrite. Kept the empty-name query-item cleanup (Jellyfin's
TranscodingUrl still sometimes emits `?&...`). Docstring rewritten.
Tests
- Updated primary fixture's TranscodingUrl to the new `/master.m3u8`
shape Jellyfin now returns.
- New `progressiveTranscodingResponseJSON` fixture for a regression test.
- `liveTvOpenStreamSendsCorrectJSONBody` now asserts the request body's
TranscodingProfile fields explicitly: Container=ts, MinSegments=2,
BreakOnNonKeyFrames=true (Bool, not string), AudioCodec, VideoCodec.
- Renamed `…RewritesLiveStreamToHls` → `…UsesServerSuppliedHlsTranscodingUrl`.
- New regression test `liveTvOpenStreamPreservesProgressiveTranscodingUrl`
pins behavior: if a server (despite our HLS profile) ever returns a
`/stream` URL, the client must NOT silently rewrite it — fail loudly
instead of producing a broken HLS URL.
44/44 JellyfinAPI tests pass. Full tvOS build succeeds. Ran the full
/motif:dev workflow: 2 Claude critics in parallel + Codex second-opinion.
Critics caught the original "share one fixture across contradictory
tests" mistake (split into two fixtures), the docstring rot, and that
the kept `if liveStreamId != nil` block needed a comment explaining its
post-rewrite-deletion purpose. Codex additionally caught that hevc
should join the videoCodec list (free win — server stream-copies
instead of transcoding) and that both fixtures need top-level
LiveStreamId so the cleanup gate exercises the same code path as the
rename target.
Known limitation (deferred): the DirectPlay-for-live path still routes
to `/Videos/{id}/stream.{container}` (progressive). The user's failing
channel was rejecting DirectPlay (TranscodeReasons=DirectPlayError) so
this fix targets the actual bug. Channels that pass DirectPlay haven't
been validated on tvOS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two unrelated regressions from the three-tab redesign (9ded87f): 1. Build error at line 302: `isFocused ? .primary : .primary.opacity(0.9)` mixed `HierarchicalShapeStyle` with `some ShapeStyle` and the ternary's result couldn't unify. Fixed by collapsing into `.primary.opacity(isFocused ? 1.0 : 0.9)` — same visual, one type. 2. Visual overlap: focused channel cells in the left column extended ~24pt into the program grid because `.buttonStyle(.card)` scales the focused button by ~1.1× and the cell filled the full 240pt column width. Inset each cell by 12pt horizontally so the scaled card stays inside the column. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Serialize synthetic drags and preserve the cursor after queued WindowServer events.
Reconcile foreground, Space, and display changes without selecting stale hosted status windows.
Separate initial SwiftUI and status-item layout and coalesce panel resizing.
Activate parked items directly when possible, track the live hosted proxy, and keep storage geometry stable across foreground and display transitions.
* Fix deterministic menu bar moves * Run macOS tests on pull requests * Fix debug test host * Keep debug module import stable
* Keep Barr control visible after launch * Prepare Barr 0.0.9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
barr-v*,windo-v*, andloadout-v*) to prevent collisions.Why
The apps were spread across standalone repositories even though they share ownership, native tooling, Apple signing credentials, and Homebrew distribution. This establishes
zackbart/appsas their common source and publishing home without breaking existing release URLs during migration.Publishing impact
The new release workflow builds, signs, notarizes, publishes the selected DMG, and updates its Homebrew cask. Before the first monorepo release, the seven existing Actions secrets must be recreated on
zackbart/appsbecause GitHub does not expose secret values for copying.Validation
actionlint .github/workflows/*.ymlgit diff --checkswift buildfor HerdrKit, JellyfinAPI, LiveTV, and Loadout's HerdrKitFull Xcode tests were not available locally because the machine currently has Command Line Tools, not the full Xcode application. Barr CI will exercise the native test suite on GitHub's
macos-26runner.