From b70c07d30ee0bf408b7776628024dce657514003 Mon Sep 17 00:00:00 2001 From: Osilvfe <64585305+Osilvfe@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:18:47 +0800 Subject: [PATCH 1/7] feat: add Android View host for AMLL renderer --- .../java/dev/yaqmc/amll/ui/AMLLPlayerView.kt | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 amll/src/main/java/dev/yaqmc/amll/ui/AMLLPlayerView.kt diff --git a/amll/src/main/java/dev/yaqmc/amll/ui/AMLLPlayerView.kt b/amll/src/main/java/dev/yaqmc/amll/ui/AMLLPlayerView.kt new file mode 100644 index 0000000..7171657 --- /dev/null +++ b/amll/src/main/java/dev/yaqmc/amll/ui/AMLLPlayerView.kt @@ -0,0 +1,89 @@ +package dev.yaqmc.amll.ui + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import dev.yaqmc.amll.model.LyricLine +import dev.yaqmc.amll.model.MaskObsceneWordsMode +import dev.yaqmc.amll.state.AMLLPlayerState + +/** + * Android View host for applications that are not themselves Compose-first. + * + * The renderer remains [AMLLPlayer]. This wrapper owns the Compose composition and exposes + * imperative playback and lyric updates so a traditional Activity, Fragment, Capacitor host, + * or other View hierarchy can mount AMLL without declaring a host-side `@Composable`. + * + * Calls that mutate renderer state should be made from the Android main thread, like normal View + * mutations. + */ +class AMLLPlayerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : AbstractComposeView(context, attrs, defStyleAttr) { + private val playerState = AMLLPlayerState() + + /** Current renderer style. Assigning a new value recomposes the hosted player. */ + var style: AMLLStyle by mutableStateOf(AMLLStyle()) + + /** Invoked when the user taps a lyric line. */ + var onLineClick: ((LyricLine) -> Unit)? by mutableStateOf(null) + + /** Exposes the underlying state for advanced integrations while simple hosts stay imperative. */ + val state: AMLLPlayerState + get() = playerState + + init { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + } + + fun setLyricLines(lines: List) { + playerState.setLyricLines(lines) + } + + fun update(positionMs: Long, isPlaying: Boolean) { + playerState.update(positionMs = positionMs, isPlaying = isPlaying) + } + + fun updatePosition(positionMs: Long) { + playerState.update(positionMs) + } + + fun updatePlaybackState(isPlaying: Boolean) { + playerState.updatePlaybackState(isPlaying) + } + + fun seekTo(positionMs: Long) { + playerState.seekTo(positionMs) + } + + fun setAutoSeekDetectionEnabled(enabled: Boolean) { + playerState.updateAutoSeekDetectionEnabled(enabled) + } + + fun setMaskObsceneWordsMode(mode: MaskObsceneWordsMode) { + playerState.updateMaskObsceneWordsMode(mode) + } + + fun setMaskObsceneWordChar(char: Char) { + playerState.updateMaskObsceneWordChar(char) + } + + @Composable + override fun Content() { + AMLLPlayer( + state = playerState, + modifier = Modifier.fillMaxSize(), + style = style, + onLineClick = onLineClick, + ) + } +} From b18714d403f141ca4fff1da0a18f567d200800d1 Mon Sep 17 00:00:00 2001 From: Osilvfe <64585305+Osilvfe@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:18:53 +0800 Subject: [PATCH 2/7] test: add View-host consumer smoke settings --- .../view-consumer-smoke/settings.gradle.kts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 integration/view-consumer-smoke/settings.gradle.kts diff --git a/integration/view-consumer-smoke/settings.gradle.kts b/integration/view-consumer-smoke/settings.gradle.kts new file mode 100644 index 0000000..1c9e9d1 --- /dev/null +++ b/integration/view-consumer-smoke/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenLocal() + google() + mavenCentral() + } +} + +rootProject.name = "amll-view-consumer-smoke" From 3370c6013a85a0a62fae7152e3aadba142bd65b1 Mon Sep 17 00:00:00 2001 From: Osilvfe <64585305+Osilvfe@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:18:58 +0800 Subject: [PATCH 3/7] test: add non-Compose View consumer smoke build --- .../view-consumer-smoke/build.gradle.kts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 integration/view-consumer-smoke/build.gradle.kts diff --git a/integration/view-consumer-smoke/build.gradle.kts b/integration/view-consumer-smoke/build.gradle.kts new file mode 100644 index 0000000..ee8b99a --- /dev/null +++ b/integration/view-consumer-smoke/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("com.android.library") version "8.13.0" + id("org.jetbrains.kotlin.android") version "2.2.20" +} + +android { + namespace = "dev.yaqmc.amll.viewconsumer" + compileSdk = 36 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlinOptions { + jvmTarget = "21" + } +} + +dependencies { + implementation("dev.yaqmc:amll-android:0.1.0-SNAPSHOT") +} From 8d1a668f9790dab4b2c2106e8087ae47a339b54c Mon Sep 17 00:00:00 2001 From: Osilvfe <64585305+Osilvfe@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:19:05 +0800 Subject: [PATCH 4/7] test: enable AndroidX for View consumer smoke --- integration/view-consumer-smoke/gradle.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 integration/view-consumer-smoke/gradle.properties diff --git a/integration/view-consumer-smoke/gradle.properties b/integration/view-consumer-smoke/gradle.properties new file mode 100644 index 0000000..5bac8ac --- /dev/null +++ b/integration/view-consumer-smoke/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true From b1908e5502fc8a9e6348f9d3ce7b0aaa7b7b4a91 Mon Sep 17 00:00:00 2001 From: Osilvfe <64585305+Osilvfe@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:19:10 +0800 Subject: [PATCH 5/7] test: add View consumer smoke manifest --- integration/view-consumer-smoke/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) create mode 100644 integration/view-consumer-smoke/src/main/AndroidManifest.xml diff --git a/integration/view-consumer-smoke/src/main/AndroidManifest.xml b/integration/view-consumer-smoke/src/main/AndroidManifest.xml new file mode 100644 index 0000000..94cbbcf --- /dev/null +++ b/integration/view-consumer-smoke/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + From 66fdbc6769a8a82f2a65eba87d15fe03999050f4 Mon Sep 17 00:00:00 2001 From: Osilvfe <64585305+Osilvfe@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:19:19 +0800 Subject: [PATCH 6/7] test: compile AMLLPlayerView from plain View host --- .../PublishedViewArtifactSmoke.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 integration/view-consumer-smoke/src/main/java/dev/yaqmc/amll/viewconsumer/PublishedViewArtifactSmoke.kt diff --git a/integration/view-consumer-smoke/src/main/java/dev/yaqmc/amll/viewconsumer/PublishedViewArtifactSmoke.kt b/integration/view-consumer-smoke/src/main/java/dev/yaqmc/amll/viewconsumer/PublishedViewArtifactSmoke.kt new file mode 100644 index 0000000..ba8e43b --- /dev/null +++ b/integration/view-consumer-smoke/src/main/java/dev/yaqmc/amll/viewconsumer/PublishedViewArtifactSmoke.kt @@ -0,0 +1,21 @@ +package dev.yaqmc.amll.viewconsumer + +import android.content.Context +import android.widget.FrameLayout +import dev.yaqmc.amll.ui.AMLLPlayerView + +class PublishedViewArtifactSmoke(context: Context) : FrameLayout(context) { + val lyricsView = AMLLPlayerView(context) + + init { + addView( + lyricsView, + LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + ), + ) + lyricsView.setLyricLines(emptyList()) + lyricsView.update(positionMs = 0L, isPlaying = false) + } +} From e598e7b2c2037d3f62e74e22eb9c702c873de3b0 Mon Sep 17 00:00:00 2001 From: Osilvfe <64585305+Osilvfe@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:19:26 +0800 Subject: [PATCH 7/7] ci: verify non-Compose Android View consumer --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1108d6f..f5ea1a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,5 @@ jobs: :demo:assembleDebug - name: Compile published artifact as YAQMC-style consumer run: gradle -p integration/consumer-smoke assembleDebug + - name: Compile published artifact from plain Android View host + run: gradle -p integration/view-consumer-smoke assembleDebug