- Rewrite values-lzh/strings.xml with proper Classical Chinese grammar/vocabulary
- Use ancient monosyllabic roots forming disyllabic compounds (曲次、歌選、藏書樓)
- Correct function words (之、於、以、乃、故、矣、哉、也、乎、焉)
- Avoid vernacular (的、是、在、有、這、那、麼、們、著、了、過)
- Verb-object, subject-predicate, modifier-head compounds per Classical word formation
- Add new desktop and iOS screenshots under assets/screenshots, named by screen, theme and form factor, and optimized with pngquant + oxipng
- Add feature graphics under assets/feature-graphics: Play Store feature graphic, README hero and mobile strip, and phone/desktop store shots
- Use them in the README, Flathub metainfo (plain screenshots) and F-Droid metadata (phone screenshots + featureGraphic)
- Replace the Steam library hero with one made from the new UI
- Remove the old desktop and mobile screenshots
Read sheet progress in draw/derived scopes instead of composition so a
drag no longer recomposes the player every frame, keep the expanded
player, floating player and bottom bar composed once shown instead of
building them mid-drag, and settle with a spring that carries fling
velocity. Add a drag test for CompactPlayerOverlay.
Initialize NewPipeDownloader once per process so recreating the Activity
no longer opens a second DataStore on spotube_cookies.preferences_pb.
Kill the process after PlaybackService shuts down on task removal so the
proxy server, MQTT client and singletons don't linger in the background.
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>
A beta is the stable build released as <version>-beta.N, with N set via
betaNumber in composeApp/build.gradle.kts. Betas and stable releases are
drafts; only nightly publishes immediately.
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.
The merge of dev resolved the libs.versions.toml conflict by taking the wrong
catalog: it reverted the Kotlin 2.4.21 / Compose 1.12.2 / Zipline 1.28.0
upgrade and dropped dev's jmustache entries, so buildSrc failed with
"Unresolved reference 'jmustache'". Keep both sides: mokkery 3.5.1 plus jmustache.
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
- Flatpak manifest (GNOME 51, libvlc built from source) installing from a new
portable spotube-linux-<version>-<arch>.tar.xz, shared with the AUR package
- Release workflow builds and attaches the Linux tarballs for x86_64 and aarch64
- Package files are JMustache templates rendered by :composeApp:renderPackaging<Target>
with the version, release asset checksums and git-cliff release notes; the
metainfo release history is generated
- Publish workflow (workflow_dispatch, release tag input) renders once and pushes to
Flathub, the AUR, WinGet (PR from team-spotube/winget-pkgs) and Chocolatey
- Chocolatey and WinGet updated for the NSIS installer
- 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