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 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, + ) + } +} 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") +} 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 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" 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 @@ + 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) + } +}