Compose's isSystemInDarkTheme never updates under the Tao window, so Theme.SYSTEM
stayed light. Use Nucleus' darkmode-detector on JVM and keep Compose's check on mobile.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The proxy copied every upstream header, duplicating Content-Length, which VLC
rejects. Forward only the range/validator headers. Bump vlcj-bundler to 0.2.1,
which bundles the Matroska demuxer needed for WebM. Make StreamingUrlRepository's
platform injectable and add VLC log switch via DebugConfig.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
accounts.spotify.com rejected iOS logins ("Oops, something went wrong") because
the WebView sent WKWebView's default User-Agent, which lacks Safari's
"Version/x ... Safari/604.1" and reads as an embedded browser. The random
User-Agent header never reached any request: the screen replaces the content
before it loads.
Have WebKit build Safari's exact UA (with its frozen OS version) via a probe
WKWebView using Safari's applicationNameForUserAgent, prime it at startup and
apply it through webSettings.customUserAgentString so every request uses it.
AVPlayer can't open WebM/Ogg or DASH, so the default opus/webm stream failed
silently on iOS.
- Rank streams the platform's player can play above format/quality prefs
- Default iOS streaming to aac/mp4, migrate saved WebM/Ogg prefs on iOS and
hide unplayable formats in the picker (desktop/Android keep opus/webm)
- Normalize codec/container names on iOS like androidJvm (mp4a -> aac, m4a -> mp4)
- Wait up to 30s for the audio plugin to load when resolving, so the queue
restored at startup isn't failed permanently before plugins are ready
CIO reports a failed bind from its accept coroutine; unhandled, that
exception terminates the process on Kotlin/Native before the port fallback
can run, so relaunching iOS shortly after playback crashed (EADDRINUSE).
Contain it with a parent CoroutineExceptionHandler and enable reuseAddress
so the preferred port can be rebound while old connections are in TIME_WAIT.
newSingleThreadContext threads get only 512 KiB of stack on iOS, while
QuickJS may recurse ~6 MiB deep when compiling a plugin, so the app crashed
with SIGBUS at startup once a plugin was installed. Host the dispatcher on an
NSThread with an 8 MiB stack, matching JVM/Android.
On iOS the CIO client engine competed with Darwin as the default engine and
has no TLS on Kotlin/Native, so HTTPS requests (e.g. plugin discovery) failed.
Remove ktor-client-cio and build every client through a Koin-provided
HttpClientFactory that owns one client on the platform default engine
(Darwin on iOS, OkHttp on Android/desktop). Consumers use the shared
default/json clients or derive their own via create { }, sharing the engine.
Several dependencies (coil 3.5.0, millennium, mqtt/ditchoom, composeunstyled,
markdown-renderer, composewebview, kmp-zip, filekit) now ship klibs built with
Kotlin 2.4, which Kotlin/Native 2.3 refuses to read, breaking the iOS build.
- Bump zipline (1.28.0), mokkery (3.5.1) and ksp (2.3.12) for the new compiler
- Type TOP_LEVEL_ROUTES as Set<NavKey> (Kotlin 2.4 inferred Set<Any>)
- Import ComposeApp in iOSApp.swift, which uses ExternalUriHandler
- Downloads resolve streams with the user's download format/quality via a
new StreamRequest instead of the streaming settings, and bypass the
playback stream-URL cache in both directions
- Downloads only consider progressive streams; DASH/HLS manifests were
previously saved as-is instead of audio
- NewPipe: map AAC "mp4a.40.x"/"m4a" to the advertised "aac"/"mp4" so real
AAC streams can match (previously the hard-coded aac/mp4 manifests always
won), report each stream's real delivery method, skip streams without a
format or non-URL content
- Fail the download on non-2xx responses, on a range request answered with
a full 200, and on video/* content, with localized reasons, instead of
writing the response body to disk
- Save AAC as .m4a
- Tests for request overrides, manifest exclusion and cache isolation
PlaybackService.onCreate runs on the main thread and called
runBlocking { getString(...) } for its notification strings. Compose
resources share one in-flight load per strings file (AsyncCache creates the
Deferred in the caller's scope); when App()'s stringResource started that
load on the main dispatcher first, blocking main to await it deadlocked and
the first frame never rendered. Timing-dependent, so mostly seen under the
debugger.
Create the notification channel immediately under the app name and update
its localized name/description from serviceScope. The short-lived
placeholder notification no longer carries localized text.
Downloads
- Show the real download folder: "Current folder: <path> (default)" in
settings instead of the misleading "default music folder", and a
"Saved to <path>" line on the Downloads screen
- Register finished downloads with Android's MediaStore (MediaScanner) so
music players and the Files app see them immediately
- Add tagged logs across the download path to diagnose format/quality
selection: raw NewPipe streams ([Streams]), every selection candidate
with its rank key and fallback warnings ([Resolve]), and per-download
enqueue prefs, resolved stream, HEAD/GET status + content type, segment
responses and written vs expected bytes ([Download])
Connect status
- Mobile Home keeps its Devices / Group Jam / Support icons (Jam now uses
the sound icon) and shows a slim banner only while remote-controlling a
device or in a Jam ("Playing on krtx", "Hosting · 3 listeners")
- Sidebar Devices / Group Jam rows get a live dot and status line
- New ConnectStatusViewModel feeds both
Remote control settings
- Extract "Allow remote control" and "Device name" into shared
remoteControlSettingCards, used by Playback settings and a new
"This device" section on the Devices screen
- Remove the unused "Enable Connect" setting and its strings
- Redesign RemoteControlScreen to match the expanded player: artwork sized
to fit, side-by-side queue panel on wide screens, proper connecting /
disconnected / error states, "Playing on <device>" subtitle
- Seek and volume sliders keep local drag state and only commit on release
(volume throttled while dragging) instead of sending a command per frame
- Redesign DevicesScreen with the settings card components, a connected
device card, error banner and empty state; ViewModels expose one UiState
- Fix tapping an already-connected device doing nothing; reopen its remote
screen instead, and switch devices by disconnecting first
- Fix the controller getting stuck in Connecting after the host denies a
request: Ktor never delivers Close frames to `incoming`, so detect the
close reason after the receive loop ends
- Fix a UI freeze when leaving the devices screen on desktop: run discovery
(and JmDNS's blocking close/resolve) on Dispatchers.IO
- Advertise the LAN address of the default-route interface on desktop;
dns-sd-kt otherwise picks any site-local address, e.g. a Docker bridge
NewPipe durations are seconds, not ms, so duration scoring never matched.
Fix the uploader match direction, normalize titles, and walk a ladder of
searches (ISRC, songs, videos, title only) until a confident match is found.
Return nothing rather than unrelated results. Add detailed ISRC/NewPipe logs.
Ktor's request() buffers the full body before returning, so ExoPlayer timed
out waiting for headers. Use prepareRequest().execute{} and tee the body to
the cache file, committing only complete 200 responses. Treat player
disconnects (broken pipe) as debug-level noise rather than upstream errors.