- move keepScreenAwake from video to chat settings (store + UI)
- mute section headers (lighter weight, smaller, reduced opacity)
- add breathing room between app bar and first setting
- improve labels across general, video, and chat settings
- reorder store fields to logical grouping with organized reset
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The seekToLiveEdge call was firing too aggressively — on initial load
latency reports inflated values before the player stabilizes, and
each seek interrupts playback. The refresh button handles manual
recovery if the viewer falls behind.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch to forked better_native_video_player that uses preferredPeakBitRate
instead of replaceCurrentItem for seamless quality transitions without
black flash. Fork also adds configureForLivePlayback and getLatencyToLive
APIs for low-latency tuning and HLS program date-time based latency.
Add latency polling (10s interval) with auto-sync chat delay support.
Only update syncedChatDelay on >2s drift to avoid restarting countdown.
Add cookie extraction from WKWebView login session for ad-free GQL
playback. The auth-token cookie works with the web Client-ID per
Streamlink's approach. Extract at login, persist to secure storage,
and attempt fresh extraction on init for existing logged-in users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds an opt-in native HLS video player (iOS-only, off by default) that
uses AVPlayerViewController for true automatic picture-in-picture on
swipe-home, replacing the WebView-based player when enabled.
Key changes:
- GQL API client for fetching playback access tokens and building HLS URLs
- NativeVideoStore (MobX) managing player lifecycle, quality selection, PiP
- VideoPlayerInterface abstraction so both stores drive the same overlay
- Settings toggle gated on iOS with "Native player (experimental)" label
- Auth token fallback: tries authenticated GQL for ad-free, falls back to
anonymous if rejected (app OAuth token != web Client-ID token)
- Scoped UnauthorizedInterceptor to Helix/OAuth URLs only
- iOS Info.plist background audio mode for PiP audio continuity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
History was only saved via handleQuery (keyboard submit), but
type-to-search calls _performSearch directly through the debounce
timer, bypassing history. Move the save into _performSearch so all
search paths record history.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix cold-start privacy bug: apply persisted collection preference on launch
- Disable Firebase collection in debug builds to avoid polluting prod data
- Add Crashlytics custom keys (is_logged_in, channel), user identifier, and
WebSocket reconnection breadcrumbs for actionable crash reports
- Record non-fatal errors for server failures, WebSocket errors, and global
asset fetch failures (skip timeouts/network errors to avoid noise)
- Update is_logged_in key and user identifier on login/logout transitions
- Log auth reconnection exhaustion before forced logout
- Add FirebaseAnalyticsObserver with named routes for automatic screen tracking
- Add static routeName constants to avoid duplicated string literals
- Add Firebase Performance Dio interceptor for automatic HTTP request tracing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap fullScreenButton in Observer so icon/tooltip update immediately
when toggling fullscreen. Move system UI immersive mode sync into the
videoChat Observer so it reacts to landscapeForceVerticalChat changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When showLatency was already tracking, toggling autoSyncChatDelay on
would wait up to 60s for the next tracker cycle instead of using the
cached latency value immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace JS-side `window._injected` guard with a Dart-controlled `_needsInit`
flag so `initVideo()` reliably runs after every refresh. The previous approach
loaded `about:blank` then `videoUrl` back-to-back, but `loadRequest` resolves
on navigation start (not page load), so the JS context often survived and
`onPageFinished` skipped re-initialization. Also adds an explicit `play()`
call to handle cases where the Twitch player loads in a paused state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getRecentMessage had no try/catch, so a failure from the third-party
API would silently prevent connectToChat from ever being called.
getAssets was fire-and-forget without a catchError, risking uncaught
Future errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The script used authorization code flow (response_type=code) which produces
tokens expiring in ~4 hours, while the app uses implicit flow (~60 days).
Switched the script to implicit flow to match, removing the client_secret
requirement and token exchange step.
Also fixed UnauthorizedInterceptor swallowing 401 errors without calling
handler.reject(), which left Dio request Futures hanging forever.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add selectionClick to settings switches, discrete slider steps, bottom
nav tabs, and expandable section headers. Fix intensity levels for cache
clear and random color (medium → light). Add lightImpact to thumbnail
long-press. Fix inconsistent search result long-press level. Replace
blanket lightImpact in updateNotification with targeted haptics at
error/warning call sites to eliminate double-firing and match severity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add menuTheme and menuButtonTheme to ThemeData matching the existing
overlay surface style (12px radius, outline border, compact density).
Remove per-use styling from chat_tabs.dart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
X button on secondary tabs now opens a MenuAnchor popup with
Disconnect (deactivates without removing) and Remove options.
Menu styled to match app overlay surfaces (12px radius, outline border).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge only already-activated tabs; lazy tabs join when tapped. Dedup
messages by IRC id for Twitch shared chat overlap. Preserve draft and
reply state when switching tabs in merged mode. Merge button appears
only when 2+ tabs are loaded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- extract _scheduleSystemUiModeSync to avoid redundant platform calls on
every build, deferred via addPostFrameCallback with state tracking
- use AnimatedBuilder.child so the video subtree is not rebuilt on every
animation tick during PiP spring-back
- move orientation reset from build() to initState() in home screen
- reformat landscape LayoutBuilder for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
drains delayed chat/7TV callbacks via the existing 200ms message buffer
timer instead of creating one Future.delayed per incoming event, removing
the biggest memory-pressure risk on busy channels with chat delay active
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- split chatDelay into persisted (manual) and session-only (synced) fields
so auto-synced latency values don't carry over between channels
- make latency tracker singleton/idempotent to prevent overlapping JS pollers
- add isCycling guard to prevent re-entrant latency cycles
- deduplicate concurrent stream info API requests
- tie JS cleanup timer to video mode only (skip in chat-only)
- update UI to reflect effective delay instead of persisted value
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
adds a user-controlled toggle (default on) under video settings that
controls whether wakelock is enabled during channel sessions. previously
wakelock was unconditional with no way to turn it off.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Twitch credentials from hardcoded launch.json args to a gitignored
.env file read via --dart-define-from-file. Add a shell script to get a
user auth token through the Authorization Code flow for easy emulator
login. Update README with the new setup steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 13 occurrences of BorderRadius.all(Radius.circular(8)) with a
shared kCardBorderRadius constant in constants.dart. All uses represent
the same design token: card/thumbnail corner radius across stream cards,
category cards, and their skeleton loader placeholders.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>