From 43f7ac289c85d8163d3dc41d7ce62b011a24ae34 Mon Sep 17 00:00:00 2001 From: DatLag Date: Tue, 30 Apr 2024 16:47:02 +0200 Subject: [PATCH] support deep linking --- .../datlag/aniflow/anilist/model/Medium.kt | 2 +- composeApp/build.gradle.kts | 5 -- .../src/androidMain/AndroidManifest.xml | 20 +++++++ .../kotlin/dev/datlag/aniflow/App.kt | 4 -- .../kotlin/dev/datlag/aniflow/MainActivity.kt | 56 +++++++++++++++++-- .../datlag/aniflow/module/NetworkModule.kt | 17 +----- .../dev/datlag/aniflow/other/UserHelper.kt | 48 ++++++---------- .../aniflow/ui/navigation/RootComponent.kt | 19 +++++-- .../aniflow/ui/navigation/RootConfig.kt | 4 +- .../initial/home/component/AiringCard.kt | 21 +------ .../navigation/screen/medium/MediumScreen.kt | 10 +++- .../screen/medium/MediumScreenComponent.kt | 16 ++---- .../datlag/aniflow/ui/theme/SchemeTheme.kt | 35 +----------- gradle/libs.versions.toml | 2 +- 14 files changed, 129 insertions(+), 130 deletions(-) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt index 7a9ae2b..53db389 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt @@ -484,6 +484,6 @@ data class Medium( } companion object { - private const val SITE_URL = "https://anilist.co/" + private const val SITE_URL = "https://anilist.co/anime/" } } diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index b4d2abe..2e64758 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -107,7 +107,6 @@ kotlin { implementation(libs.aboutlibraries) implementation(libs.kasechange) - implementation(libs.oidc) implementation(libs.kache) implementation("dev.datlag.sheets-compose-dialogs:rating:2.0.0-SNAPSHOT") @@ -168,10 +167,6 @@ android { multiDexEnabled = true vectorDrawables.useSupportLibrary = true - - addManifestPlaceholders( - mapOf("oidcRedirectScheme" to "aniflow") - ) } packaging { resources { diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index bc6d186..915517e 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -45,6 +45,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt index 1b00e14..21a429b 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt @@ -11,7 +11,6 @@ import dev.datlag.sekret.NativeLoader import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import org.kodein.di.* -import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory class App : MultiDexApplication(), DIAware { @@ -19,9 +18,6 @@ class App : MultiDexApplication(), DIAware { bindSingleton { applicationContext } - bindEagerSingleton { - AndroidCodeAuthFlowFactory(useWebView = false) - } import(NetworkModule.di) } diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt index 7e2458e..28869b2 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt @@ -1,25 +1,38 @@ package dev.datlag.aniflow +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope import com.arkivanov.decompose.DefaultComponentContext +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.defaultComponentContext +import com.arkivanov.decompose.handleDeepLink import com.arkivanov.essenty.backhandler.backHandler import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleOwner import com.arkivanov.essenty.lifecycle.essentyLifecycle +import com.arkivanov.essenty.statekeeper.stateKeeper +import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.navigation.RootComponent +import dev.datlag.tooling.compose.launchIO import dev.datlag.tooling.decompose.lifecycle.LocalLifecycleOwner import dev.datlag.tooling.safeCast +import io.github.aakira.napier.Napier import org.kodein.di.DIAware import org.kodein.di.instance -import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory class MainActivity : AppCompatActivity() { + private lateinit var root: RootComponent + + @OptIn(ExperimentalDecomposeApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -30,10 +43,8 @@ class MainActivity : AppCompatActivity() { val lifecycleOwner = object : LifecycleOwner { override val lifecycle: Lifecycle = essentyLifecycle() } - val factory by di.instance() - factory.registerActivity(this) - val root = RootComponent( + root = RootComponent( componentContext = DefaultComponentContext( lifecycle = lifecycleOwner.lifecycle, backHandler = backHandler() @@ -54,4 +65,41 @@ class MainActivity : AppCompatActivity() { } } } + + @SuppressLint("MissingSuperCall") + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + val uri = intent.data ?: return + val itemId = uri.pathSegments?.firstNotNullOfOrNull { it.trim().toIntOrNull() } + if (itemId != null && ::root.isInitialized) { + root.onDeepLink(itemId) + return + } + + val accessToken = uri.getFragmentOrQueryParameter("access_token") + if (accessToken.isNullOrBlank()) { + return + } + + root.onLogin( + accessToken = accessToken, + expiresIn = uri.getFragmentOrQueryParameter("expires_in")?.toIntOrNull() + ) + } + + private fun Uri.getFragmentOrQueryParameter(param: String): String? { + return this.fragment.getFragmentParameter(param) ?: getQueryParameter(param)?.ifBlank { null } + } + + private fun String?.getFragmentParameter(param: String): String? { + val keys = this?.split("&").orEmpty() + keys.forEach { key -> + val values = key.split("=") + if (values[0] == param) { + return values.getOrNull(1)?.ifBlank { null } + } + } + return null + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 6523617..41f4cb6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -34,7 +34,6 @@ import dev.datlag.tooling.async.suspendCatching import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.map import org.kodein.di.bindProvider -import org.publicvalue.multiplatform.oidc.OpenIdConnectClient data object NetworkModule { @@ -118,26 +117,12 @@ data object NetworkModule { crashlytics = nullableFirebaseInstance()?.crashlytics ) } - bindSingleton(Constants.AniList.Auth.CLIENT) { - OpenIdConnectClient { - endpoints { - baseUrl(Constants.AniList.Auth.BASE_URL) { - authorizationEndpoint = "authorize" - tokenEndpoint = "token" - } - } - clientId = Sekret.anilistClientId(BuildKonfig.packageName) - clientSecret = Sekret.anilistClientSecret(BuildKonfig.packageName) - redirectUri = Constants.AniList.Auth.REDIRECT_URL - } - } bindSingleton { UserHelper( userSettings = instance(), appSettings = instance(), client = instance(Constants.AniList.APOLLO_CLIENT), - authFlowFactory = instance(), - oidc = instance(Constants.AniList.Auth.CLIENT) + clientId = Sekret.anilistClientId(BuildKonfig.packageName)!! ) } bindSingleton { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt index d765c0f..759da06 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt @@ -1,5 +1,9 @@ package dev.datlag.aniflow.other +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import dev.datlag.aniflow.anilist.ViewerMutation @@ -11,22 +15,23 @@ import dev.datlag.aniflow.model.safeFirstOrNull import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.async.suspendCatching +import dev.datlag.tooling.compose.withIOContext import dev.datlag.tooling.compose.withMainContext import kotlinx.coroutines.flow.* +import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock -import org.publicvalue.multiplatform.oidc.OpenIdConnectClient -import org.publicvalue.multiplatform.oidc.appsupport.CodeAuthFlowFactory -import org.publicvalue.multiplatform.oidc.types.remote.AccessTokenResponse +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds class UserHelper( private val userSettings: Settings.PlatformUserSettings, private val appSettings: Settings.PlatformAppSettings, private val client: ApolloClient, - private val authFlowFactory: CodeAuthFlowFactory, - private val oidc: OpenIdConnectClient + private val clientId: String ) { val isLoggedIn: Flow = userSettings.isAniListLoggedIn.distinctUntilChanged() + val loginUrl: String = "https://anilist.co/api/v2/oauth/authorize?client_id=$clientId&response_type=token" private val changedUser: MutableStateFlow = MutableStateFlow(null) private val userQuery = client.query( @@ -65,26 +70,6 @@ class UserHelper( ) } - suspend fun login(): Boolean { - if (isLoggedIn.safeFirstOrNull() == true) { - return true - } - - val flow = withMainContext { - authFlowFactory.createAuthFlow(oidc) - } - - val tokenResult = suspendCatching { - flow.getAccessToken() - } - - tokenResult.getOrNull()?.let { - updateStoredToken(it) - } - - return tokenResult.isSuccess - } - suspend fun updateAdultSetting(value: Boolean) { appSettings.setAdultContent(value) changedUser.emit( @@ -127,12 +112,15 @@ class UserHelper( } } - private suspend fun updateStoredToken(tokenResponse: AccessTokenResponse) { + suspend fun saveLogin( + accessToken: String, + expiresIn: Int?, + ) { userSettings.setAniListTokens( - access = tokenResponse.access_token, - expires = tokenResponse.expires_in?.let { - Clock.System.now().epochSeconds + it - }?.toInt() + access = accessToken, + expires = expiresIn?.let { + Clock.System.now().plus(it.seconds).epochSeconds.toInt() + } ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt index bcc88d5..35c9771 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt @@ -7,20 +7,21 @@ import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.push +import com.arkivanov.decompose.router.stack.* import dev.datlag.aniflow.common.onRender +import dev.datlag.aniflow.model.ifValueOrNull +import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.navigation.screen.initial.InitialScreenComponent import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent import org.kodein.di.DI +import org.kodein.di.instance class RootComponent( componentContext: ComponentContext, override val di: DI ) : Component, ComponentContext by componentContext { + private val userHelper by instance() private val navigation = StackNavigation() private val stack = childStack( source = navigation, @@ -70,4 +71,14 @@ class RootComponent( } } } + + fun onDeepLink(mediumId: Int) { + navigation.replaceAll(RootConfig.Home, RootConfig.Details(mediumId)) + } + + fun onLogin(accessToken: String, expiresIn: Int?) { + launchIO { + userHelper.saveLogin(accessToken, expiresIn) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt index 7fa42c1..1fed75e 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt @@ -10,5 +10,7 @@ sealed class RootConfig { data object Home : RootConfig() @Serializable - data class Details(val medium: Medium) : RootConfig() + data class Details(val medium: Medium) : RootConfig() { + constructor(id: Int) : this(Medium(id)) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt index 0f655ca..86c2cea 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt @@ -21,7 +21,6 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.preferred import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.model.AppSettings -import dev.datlag.aniflow.ui.theme.LocalDominantColorState import dev.datlag.aniflow.ui.theme.SchemeTheme import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow @@ -36,8 +35,6 @@ fun AiringCard( modifier: Modifier = Modifier, onClick: (Medium) -> Unit ) { - val schemeState = LocalDominantColorState.current - airing.media?.let(::Medium)?.let { media -> Card( modifier = modifier, @@ -65,27 +62,15 @@ fun AiringCard( model = media.coverImage.medium, contentScale = ContentScale.Crop, onSuccess = { state -> - if (schemeState != null) { - scope.launch { - schemeState.updateFrom(state.painter) - } - } + } ), onSuccess = { state -> - if (schemeState != null) { - scope.launch { - schemeState.updateFrom(state.painter) - } - } + } ), onSuccess = { state -> - if (schemeState != null) { - scope.launch { - schemeState.updateFrom(state.painter) - } - } + } ) Column( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt index bfe15e5..eea28da 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.maxkeppeker.sheets.core.models.base.Header @@ -28,15 +29,18 @@ import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials +import dev.datlag.aniflow.LocalDI import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.type.MediaStatus import dev.datlag.aniflow.common.* import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.custom.EditFAB import dev.datlag.aniflow.ui.navigation.screen.medium.component.* import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import org.kodein.di.instance @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, ExperimentalFoundationApi::class) @Composable @@ -111,6 +115,8 @@ fun MediumScreen(component: MediumComponent) { ) if (!notReleased) { + val uriHandler = LocalUriHandler.current + EditFAB( displayAdd = !alreadyAdded, bsAvailable = component.bsAvailable, @@ -119,9 +125,7 @@ fun MediumScreen(component: MediumComponent) { }, onRate = { - component.rate { - ratingState.show() - } + }, onProgress = { // ratingState.show() diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt index 1d3a0e1..4ff7a56 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt @@ -38,8 +38,6 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.kodein.di.DI import org.kodein.di.instance -import org.publicvalue.multiplatform.oidc.OpenIdConnectClient -import org.publicvalue.multiplatform.oidc.appsupport.CodeAuthFlowFactory import kotlin.time.Duration.Companion.seconds class MediumScreenComponent( @@ -275,15 +273,13 @@ class MediumScreenComponent( override fun rate(onLoggedIn: () -> Unit) { launchIO { - if (userHelper.login()) { - val currentRating = rating.safeFirstOrNull() ?: initialMedium.entry?.score?.toInt() ?: -1 - if (currentRating <= -1) { - requestMediaListEntry() - } + val currentRating = rating.safeFirstOrNull() ?: initialMedium.entry?.score?.toInt() ?: -1 + if (currentRating <= -1) { + requestMediaListEntry() + } - withMainContext { - onLoggedIn() - } + withMainContext { + onLoggedIn() } } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt index 4659626..58703b1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt @@ -44,31 +44,6 @@ data object SchemeTheme { } } -@Composable -fun rememberSchemeThemeDominantColor( - key: Any?, - state: DominantColorState? = null, -): Color? { - if (key == null) { - return null - } - - val fallbackState = remember(state) { - state - } ?: remember(key) { - SchemeTheme.kache.getIfAvailable(key) - } ?: rememberPainterDominantColorState( - coroutineContext = ioDispatcher() - ) - val useState by produceState(fallbackState, key) { - value = withIOContext { - SchemeTheme.kache.getOrPut(key) { fallbackState } - } ?: fallbackState - } - - return remember(useState) { useState.color } -} - @Composable fun rememberSchemeThemeDominantColorState( key: Any?, @@ -131,22 +106,16 @@ fun rememberSchemeThemeDominantColorState( ) } -val LocalDominantColorState = compositionLocalOf?>{ null } - @Composable fun SchemeTheme(key: Any?, content: @Composable (DominantColorState) -> Unit) { val state = rememberSchemeThemeDominantColorState(key) DynamicMaterialTheme( - seedColor = rememberSchemeThemeDominantColor(key, state) ?: MaterialTheme.colorScheme.primary, + seedColor = state.color, useDarkTheme = LocalDarkMode.current, animate = true ) { - CompositionLocalProvider( - LocalDominantColorState provides state, - ) { - content(state) - } + content(state) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd3c205..b548106 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ coroutines = "1.8.0" crashlytics-plugin = "2.9.9" datastore = "1.1.0" datetime = "0.6.0-RC.2" -decompose = "3.0.0-beta01" +decompose = "3.0.0" desugar = "2.0.4" firebase = "1.11.1" firebase-android = "20.4.3"