mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 18:23:59 +00:00
12 KiB
12 KiB
AGENTS
Repo shape (KMP + modules)
- Gradle multi-module:
:composeApp(main app),:plugin_interfaces(plugin API contracts),:js_plugin_example(Zipline JS plugin template). :composeAppuses custom KMP source sets (mobileMain,androidJvmMain) with explicitdependsOn; Gradle prints "Default Kotlin Hierarchy Template Not Applied Correctly" warning. Treat as known/expected.
Real app entrypoints
- Desktop/JVM:
composeApp/src/jvmMain/kotlin/org/spotube/spotube/main.kt(mainClass = org.spotube.spotube.MainKt). - Android:
composeApp/src/androidMain/kotlin/org/spotube/spotube/MainActivity.kt. - iOS bridge:
composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.ktandiosApp/iosApp/ContentView.swift.
High-value commands
- Use Gradle wrapper (
./gradlewor./gradlew.bat) only. - Run desktop:
:composeApp:run - Build Android debug:
:composeApp:assembleStableDebug(orassembleNightlyDebug). Android hasstable/nightlyproduct flavors (nightly = beta channel:.nightlyapplicationId suffix, own icons/name incomposeApp/src/nightly/res). Desktop icons come fromcomposeApp/icons/{stable,nightly}, chosen with-Pnightly=true. - Module checks:
:composeApp:check,:plugin_interfaces:check,:js_plugin_example:check - Focused tests:
:composeApp:jvmTest,:composeApp:iosSimulatorArm64Test,:plugin_interfaces:jvmTest,:plugin_interfaces:jsTest,:js_plugin_example:jsTest - No lint/typecheck/formatter tasks are configured;
:composeApp:checkis the only aggregated check.
Dependencies
gradle/libs.versions.tomlis the single source of truth for all version pins and library declarations.- Kotlin:
2.4.21, JVM target:11(compile/target compatibility in bothcomposeApp/build.gradle.ktsandplugin_interfaces/build.gradle.kts). - compose-webview (desktop) dispatcher hijack:
dev.nucleusframework:composewebviewtransitively pullsnucleus.decorated-window-tao, which registers aTaoMainDispatcherFactory(loadPriority = 100) viaMETA-INF/servicesthat hijacksDispatchers.Mainaway from the Swing EDT. This breaks lifecycle'senforceMainThreadIfNeeded(window fails with "Method addObserver must be called on the main thread"). The app overrides it withSwingMainDispatcherFactory(composeApp/src/jvmMain/.../core/coroutines/,loadPriority = Int.MAX_VALUE) which pinsDispatchers.Mainback to the Swing EDT. Keep that factory + itsMETA-INF/services/kotlinx.coroutines.internal.MainDispatcherFactoryresource; the long-term fix is in the webview (make the Tao dispatcher opt-in or lower its priority).
Networking
- Never call
HttpClient(...)directly. InjectHttpClientFactory(commonMain/.../core/network/HttpClientFactory.kt, a Koin single) and use.default/.json(shared, never close them) or.create { ... }for extra plugins (caller owns and may close it). It owns the one engine-backed client, so the platform default engine is used everywhere (Darwin on iOS, OkHttp on Android/desktop). - Do not add
ktor-client-cio: on iOS it competes with Darwin as the default engine and has no TLS on Kotlin/Native, so every HTTPS request fails.
GraalVM native image (desktop)
- Enabled via
graalvm { }insidenucleus.applicationincomposeApp/build.gradle.kts. Nucleus downloads GraalVM CE itself (or uses$GRAALVM_HOME). Linux needsgcc+patchelf. - Build:
:composeApp:createGraalvmNativeDistributable(outputcomposeApp/build/compose/binaries/main/graalvm-app/), installers viapackageGraalvm{Deb,Rpm,AppImage,Dmg,Msi,Exe}. A build takes ~10 min and uses all cores and ~15 GB RAM. - Hand-maintained + agent-recorded metadata lives in
composeApp/graalvm/reachability-metadata.json. After adding features that use reflection/JNI/JNA (vlcj, Zipline, plugins), run:composeApp:runWithNativeAgent, exercise the feature, then close the window normally (the merge only happens on a clean exit). uk.co.caprica.vlcj.binding.lib.LibVlcmust stay registered (allDeclaredMethods+jniAccessible): JNA direct mapping finds itsnativemethods by reflection.- The
*UberJarForCurrentOStask excludes other OSes'/CPUs' native libs and stale CEF binaries, because native-image embeds every registered resource into the binary. On Linux,copyGraalvmJvmSois disabled andcopyGraalvmAwtSoLibsprefers native-image's own libs (10 KBlibjvm.so/libjava.soshims instead of the JDK's 28 MBlibjvm.so). isProductionBuild(top ofcomposeApp/build.gradle.kts) is true forpackage*/create*/notarize*/nativeImageCompiletasks, or with-Pproduction=true|false. It gates the Rust release profile (GobleyjvmVariant) so:composeApp:runkeeps fast debug Rust builds. Put new slow, ship-only optimizations behind it.- Full user-facing build docs are in
README.md("Building from source").
Codegen and plugin packaging
- JS plugin bundles:
:js_plugin_example:packageDevelopmentPluginand:js_plugin_example:packageProductionPlugin. Output is.smplugfiles injs_plugin_example/build/distributions/. - Zipline plugin entrypoint:
mainFunction = "org.spotube.js_plugin_example.main"injs_plugin_example/build.gradle.kts. Plugin metadata fromjs_plugin_example/plugin.json.
Plugin architecture
plugin_interfacesexportszipline.coreandsemveras API.composeAppdepends on it for the plugin system.plugin_interfacesalso has a JS target (browser()), used by the plugin system.
UI component patterns
- ALL user-facing strings MUST come from
composeApp/src/commonMain/composeResources/values/strings.xmlviastringResource(Res.string.xxx). Never hardcode a literal string in aText(...), dialog title, button label, content description, placeholder, or error message inside@Composablecode. If the string you need doesn't exist yet, add a new<string name="...">entry tostrings.xml(grouped near related keys, following the existingfeature_context_purposenaming convention) and reference it — do not inline the text "just this once". This applies to every module, including ones you're about to add (dialogs, screens, snackbars, etc.), not justmodules/settings.strings.xmlis the single source of truth for copy, not a showcase file for one screen. - ViewModels own ALL state and logic. Composables are dumb renderers: they collect a single
StateFlow<UiState>from the ViewModel and forward user events (clicks, text input, drag callbacks) back to ViewModel functions. No business logic, filtering, derivation, reordering buffers, orLaunchedEffect-based state syncing belongs in a composable — it goes in the ViewModel. Thecombine/stateInflow chain in the ViewModel must produce fully-computed, ready-to-render UI state so the composable never needs intermediaterememberderivations ormutableStateListOfmirrors. - AdaptiveDropdownBottomSheet (
commonMain/.../core/ui/component/AdaptiveDropdownBottomSheet.kt): switches betweenDropdownMenu(large screen) andModalBottomSheet(small screen) viacurrentWindowAdaptiveInfo(). Do NOT use expect/actual — all adaptive components that rely ONLY on Compose/Material3 APIs belong in commonMain. - AdaptiveDialogBottomSheet (
commonMain/.../core/ui/component/AdaptiveDialogBottomSheet.kt): switches betweenThemedDialog(large screen) andModalBottomSheet(small screen) viacurrentWindowAdaptiveInfo(). Same rule — keep in commonMain unless platform-specific APIs are required. - Use
expect/actualonly when the component MUST use platform-specific APIs (e.g.WindowStatefor desktop window controls, native scrollbars). Pure Compose/Material3 adaptivity stays in commonMain. - JavaFX is required;
--add-opensflags incompose.desktop.application.jvmArgsmust be preserved:javafx.graphics/javafx.scene,javafx.graphics/com.sun.javafx.sg.prism,javafx.graphics/com.sun.javafx.scene,javafx.web/com.sun.webkit,javafx.media/com.sun.media.jfxmedia,javafx.media/com.sun.media.jfxmedia.events. - JavaFX dependencies are loaded from OpenJFX with platform classifiers (win/mac/linux) resolved at configuration time via
System.getProperty("os.name").
Tooling
- Gradle config cache enabled (
gradle.properties); prefer module-scoped tasks. - Gradle daemon JVM pinned to JetBrains JDK 21 via
gradle/gradle-daemon-jvm.properties. - Gradle 8.14.3 (from
gradle/wrapper/gradle-wrapper.properties). - Foojay toolchain resolver in use (
plugins { id("org.gradle.toolchains.foojay-resolver-convention") }).
Current testing reality
- No committed
*Test*.ktfiles; test tasks may run zero tests unless new tests are added.
Release workflow (.github/workflows/release.yml)
- Triggered by
workflow_dispatchwith arelease_typechoice input (stable|nightly). prepare-depsjob checks out & publisheskdroidFilter/ComposeNativeWebview(compose-webview) tomavenLocal. The spotube gradle plugin and vlcj-bundler now come from Maven Central.- Build jobs per platform (Android, Linux, Windows, macOS), all
needs: prepare-deps. Desktop jobs ship GraalVM native installers (packageGraalvm{Deb,Rpm,Exe,Dmg}, output incomposeApp/build/compose/binaries/main/graalvm-<format>/), not jpackage/JVM ones. - Stable: reads
versionNamefromcomposeApp/build.gradle.kts, tag =v{version}, draft release. - Nightly: builds with
-PversionName=nightly, tag =nightly(updates existing), prerelease. - Android signing: decodes
secrets.KEYSTORE(base64) →composeApp/upload-keystore.jks, writes signing config intolocal.propertiesfrom secrets. create-releasejob (depends on all builds) usessoftprops/action-gh-release@v2to create the GitHub release with all artifacts attached.
Package managers (packaging/, .github/workflows/spotube-publish-binary.yml)
packaging/<target>(linux,aur,flatpak,winget,chocolatey) holds JMustache templates (*.mustache) plus files copied as is.:composeApp:renderPackaging<Target>renders them intocomposeApp/build/packaging/<target>(RenderPackagingTaskin buildSrc documents the template data). Never hand-write versions, checksums or release notes into these files.- Checksums come from release assets in
-PpackagingArtifactsDir=<dir>; a template using a missing value fails the render. Release notes come fromgit-cliff --contextwithcliff.toml(GitCliffContextTask): needsgit-cliffon the PATH and full git history.gradle.propertiesdefaults tonightly=true, which leaves commits unreleased: pass-Pnightly=falsefor a stable render. packaging/package-linux-tarball.shpacksspotube-linux-<version>-<arch>.tar.xz(the AUR and Flathub source) from the GraalVM distributable and the renderedlinuxdir; the release workflow builds it before the AppImage step.- The publish workflow (
workflow_dispatch, inputtag) renders once, then pushes to Flathub (team-spotube/org.spotube.spotube), the AUR (spotube-bin), WinGet (PR fromteam-spotube/winget-pkgs) and Chocolatey.
OpenCode Agent MCP Execution Directives
1. Code Discovery & Context Search
- Use
android-studio-mcpsearch/symbol tools to locate declarations before making edits across:composeApp,:plugin_interfaces, or:js_plugin_example[cite: 1, 2]. - Keep
gradle/libs.versions.tomlas the single source of truth for dependencies and version updates.
2. UI Architecture Constraints
- Strict ViewModel Separation: UI composables in
commonMainmust only collectStateFlow<UiState>and emit event callbacks. Do NOT write business logic, filtering, ormutableStateListOfdirectly inside@Composablefunctions. - Adaptive Components: Use
AdaptiveDropdownBottomSheetorAdaptiveDialogBottomSheetinsidecommonMainfor adaptive layouts[cite: 2]. Do NOT create platform-specificexpect/actualvariants unless platform-native APIs (like windowing or native scrollbars) are explicitly required[cite: 2].
3. Verification & Diagnostic Loops
- Post-Edit Verification: After editing Kotlin code, validate compilation by running
./gradlew :composeApp:checkor./gradlew :composeApp:assembleStableDebugvia MCP[cite: 2]. - Dispatcher Integrity: Never remove or bypass
SwingMainDispatcherFactoryincomposeApp/src/jvmMain/.../core/coroutines/[cite: 2]. It is required to preventcompose-webviewfrom hijackingDispatchers.Mainaway from the Swing EDT[cite: 2]. - Live Preview Iteration: Use
compose-hot-reloadto evaluate UI layout changes headlessly without forcing full Gradle rebuilds.