diff --git a/api/anilist/build.gradle.kts b/api/anilist/build.gradle.kts index 8feab45a..a9a9f6b4 100644 --- a/api/anilist/build.gradle.kts +++ b/api/anilist/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin) @@ -28,6 +26,8 @@ kotlin { } dependencies { + implementation(project(":api:preferences")) + // Apollo Kotlin implementation(libs.apollo.runtime) implementation(libs.apollo.cache.memory) @@ -35,7 +35,6 @@ dependencies { // Hilt implementation(libs.hilt.android) - implementation(libs.hilt.navigationCompose) ksp(libs.hilt.android.compiler) } diff --git a/api/anilist/src/main/graphql/MediaQuery.graphql b/api/anilist/src/main/graphql/MediaQuery.graphql index 29b2ee75..639d23bb 100644 --- a/api/anilist/src/main/graphql/MediaQuery.graphql +++ b/api/anilist/src/main/graphql/MediaQuery.graphql @@ -2,7 +2,7 @@ query MediaQuery( $id: Int, $type: MediaType ) { - media: Media (id: $id, type: $type) { + media: Media(id: $id, type: $type) { bannerImage coverImage { extraLarge diff --git a/api/anilist/src/main/graphql/UserQuery.graphql b/api/anilist/src/main/graphql/UserQuery.graphql new file mode 100644 index 00000000..6706ea38 --- /dev/null +++ b/api/anilist/src/main/graphql/UserQuery.graphql @@ -0,0 +1,35 @@ +query UserQuery( + $id: Int, + $name: String, + $isModerator: Boolean, + $search: String, + $sort: [UserSort], +) { + user: User( + id: $id, + name: $name + isModerator: $isModerator + search: $search + sort: $sort + ) { + id + name + about + avatar { + large + } + bannerImage + } +} + +query Viewer { + viewer: Viewer { + id + name + about + avatar { + large + } + bannerImage + } +} diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistApiModule.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistApiModule.kt index 8cd22844..e7641d40 100644 --- a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistApiModule.kt +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistApiModule.kt @@ -2,23 +2,28 @@ package com.imashnake.animite.api.anilist import android.content.Context import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.http.HttpRequest +import com.apollographql.apollo3.api.http.HttpResponse import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory import com.apollographql.apollo3.cache.normalized.normalizedCache import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory +import com.apollographql.apollo3.network.http.HttpInterceptor +import com.apollographql.apollo3.network.http.HttpInterceptorChain import com.apollographql.apollo3.network.http.LoggingInterceptor +import com.imashnake.animite.api.preferences.PreferencesRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import javax.inject.Qualifier import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object AnilistApiModule { - - // TODO name this so we can have other apollo clients for different APIs @Provides @Singleton fun provideApolloClient( @@ -35,4 +40,45 @@ object AnilistApiModule { .normalizedCache(cacheFactory) .build() } + + @Provides + @Singleton + @AuthorizedClient + fun provideAuthorizedApolloClient( + @ApplicationContext context: Context, + httpInterceptor: HttpInterceptor + ): ApolloClient { + val cacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024) + .chain(SqlNormalizedCacheFactory(context, "apollo.db")) + return ApolloClient.Builder() + .dispatcher(Dispatchers.IO) + .serverUrl("https://graphql.anilist.co/") + .addHttpInterceptor(httpInterceptor) + .addHttpInterceptor(LoggingInterceptor(LoggingInterceptor.Level.BODY)) + .normalizedCache(cacheFactory) + .build() + } + + @Singleton + @Provides + fun provideHttpInterceptor( + preferencesRepository: PreferencesRepository + ): HttpInterceptor = object : HttpInterceptor { + override suspend fun intercept( + request: HttpRequest, + chain: HttpInterceptorChain + ): HttpResponse { + return chain.proceed( + request.newBuilder().apply { + preferencesRepository.accessToken.first()?.let { + addHeader("Authorization", "Bearer $it") + } + }.build() + ) + } + } } + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthorizedClient diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistMediaRepository.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistMediaRepository.kt index d2f0b7d7..7929947a 100644 --- a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistMediaRepository.kt +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistMediaRepository.kt @@ -11,6 +11,13 @@ import com.imashnake.animite.api.anilist.type.MediaType import kotlinx.coroutines.flow.Flow import javax.inject.Inject +/** + * Repository for fetching [MediaQuery.Media] or a list of [MediaListQuery.Medium]. + * + * @param apolloClient Default apollo client. + * @property fetchMediaList Fetches a list of [MediaListQuery.Medium]. + * @property fetchMedia Fetches detailed media: [MediaQuery.Media]. + */ class AnilistMediaRepository @Inject constructor( private val apolloClient: ApolloClient ) { diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistSearchRepository.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistSearchRepository.kt index 562ef372..6dabf2e4 100644 --- a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistSearchRepository.kt +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistSearchRepository.kt @@ -9,6 +9,12 @@ import com.imashnake.animite.api.anilist.type.MediaType import kotlinx.coroutines.flow.Flow import javax.inject.Inject +/** + * Repository for fetching media search results (e.g., search bar). + * + * @param apolloClient Default apollo client. + * @property fetchSearch Fetch a list of `search`es. + */ class AnilistSearchRepository @Inject constructor( private val apolloClient: ApolloClient ) { diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistUserRepository.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistUserRepository.kt new file mode 100644 index 00000000..386e9434 --- /dev/null +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistUserRepository.kt @@ -0,0 +1,24 @@ +package com.imashnake.animite.api.anilist + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.cache.normalized.FetchPolicy +import com.apollographql.apollo3.cache.normalized.fetchPolicy +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +/** + * Repository for anything user related. Including the [ViewerQuery.Viewer]. + * + * @param apolloClient Client with the [`Authorization` header](https://anilist.gitbook.io/anilist-apiv2-docs/overview/oauth/implicit-grant#making-authenticated-requests). + * @property fetchViewer Fetches the current user with an authorized [apolloClient]. + */ +class AnilistUserRepository @Inject constructor( + @AuthorizedClient private val apolloClient: ApolloClient +) { + fun fetchViewer(): Flow> { + return apolloClient + .query(ViewerQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork).toFlow() + .asResult { it.viewer!! } + } +} diff --git a/api/preferences/build.gradle.kts b/api/preferences/build.gradle.kts new file mode 100644 index 00000000..8040355e --- /dev/null +++ b/api/preferences/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin) + alias(libs.plugins.hilt) + alias(libs.plugins.ksp) +} + +android { + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + namespace = "com.imashnake.animite.api.preferences" +} + +kotlin { + jvmToolchain(17) +} + +dependencies { + // Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + + // DataStore + implementation(libs.datastore) +} diff --git a/api/preferences/src/main/AndroidManifest.xml b/api/preferences/src/main/AndroidManifest.xml new file mode 100644 index 00000000..cc947c56 --- /dev/null +++ b/api/preferences/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/DataStoreModule.kt b/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/DataStoreModule.kt new file mode 100644 index 00000000..6d763089 --- /dev/null +++ b/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/DataStoreModule.kt @@ -0,0 +1,25 @@ +package com.imashnake.animite.api.preferences + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + @Singleton + @Provides + fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore { + return PreferenceDataStoreFactory.create( + produceFile = { appContext.preferencesDataStoreFile("default") } + ) + } +} diff --git a/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/PreferencesRepository.kt b/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/PreferencesRepository.kt new file mode 100644 index 00000000..ddb5b36f --- /dev/null +++ b/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/PreferencesRepository.kt @@ -0,0 +1,19 @@ +package com.imashnake.animite.api.preferences + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.imashnake.animite.api.preferences.ext.getValue +import com.imashnake.animite.api.preferences.ext.setValue +import javax.inject.Inject + +class PreferencesRepository @Inject constructor( + private val dataStore: DataStore +) { + private val accessTokenKey = stringPreferencesKey("access_token") + val accessToken = dataStore.getValue(accessTokenKey, null) + + suspend fun setAccessToken(accessToken: String?) { + dataStore.setValue(accessTokenKey, accessToken) + } +} diff --git a/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/ext/DataStoreExt.kt b/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/ext/DataStoreExt.kt new file mode 100644 index 00000000..c20beb45 --- /dev/null +++ b/api/preferences/src/main/kotlin/com/imashnake/animite/api/preferences/ext/DataStoreExt.kt @@ -0,0 +1,21 @@ +package com.imashnake.animite.api.preferences.ext + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.map + +fun DataStore.getValue( + key: Preferences.Key, + default: T? = null +) = data.map { + it[key] ?: default +} + +suspend fun DataStore.setValue( + key: Preferences.Key, + value: T? +) = edit { + if (value != null) it[key] = value + else it.remove(key) +} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f705ea23..d1eab698 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1caca10c..fdcec69d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,14 @@ + + + + + + diff --git a/app/src/main/java/com/imashnake/animite/features/home/Home.kt b/app/src/main/java/com/imashnake/animite/features/home/Home.kt index 556f3ab2..efdb4b7d 100644 --- a/app/src/main/java/com/imashnake/animite/features/home/Home.kt +++ b/app/src/main/java/com/imashnake/animite/features/home/Home.kt @@ -51,7 +51,7 @@ import com.imashnake.animite.core.extensions.bannerParallax import com.imashnake.animite.core.extensions.landscapeCutoutPadding import com.imashnake.animite.core.ui.ProgressIndicator import com.imashnake.animite.core.ui.TranslucentStatusBarLayout -import com.imashnake.animite.data.Resource +import com.imashnake.animite.core.data.Resource import com.imashnake.animite.features.destinations.MediaPageDestination import com.imashnake.animite.features.media.MediaPageArgs import com.imashnake.animite.features.ui.MediaSmall diff --git a/app/src/main/java/com/imashnake/animite/features/home/HomeViewModel.kt b/app/src/main/java/com/imashnake/animite/features/home/HomeViewModel.kt index 8ceb7a1e..463e2123 100644 --- a/app/src/main/java/com/imashnake/animite/features/home/HomeViewModel.kt +++ b/app/src/main/java/com/imashnake/animite/features/home/HomeViewModel.kt @@ -6,8 +6,8 @@ import androidx.lifecycle.viewModelScope import com.imashnake.animite.api.anilist.AnilistMediaRepository import com.imashnake.animite.api.anilist.type.MediaSort import com.imashnake.animite.api.anilist.type.MediaType -import com.imashnake.animite.data.Resource -import com.imashnake.animite.data.Resource.Companion.asResource +import com.imashnake.animite.core.data.Resource +import com.imashnake.animite.core.data.Resource.Companion.asResource import com.imashnake.animite.dev.ext.nextSeason import com.imashnake.animite.dev.ext.season import com.imashnake.animite.dev.internal.Constants diff --git a/app/src/main/java/com/imashnake/animite/features/navigationbar/NavigationBar.kt b/app/src/main/java/com/imashnake/animite/features/navigationbar/NavigationBar.kt index 6d8c6abc..0310d86e 100644 --- a/app/src/main/java/com/imashnake/animite/features/navigationbar/NavigationBar.kt +++ b/app/src/main/java/com/imashnake/animite/features/navigationbar/NavigationBar.kt @@ -30,6 +30,7 @@ import com.imashnake.animite.profile.ProfileNavGraph import com.imashnake.animite.rslash.RslashNavGraph import com.ramcosta.composedestinations.spec.DestinationSpec import com.ramcosta.composedestinations.utils.currentDestinationAsState +import com.ramcosta.composedestinations.utils.isRouteOnBackStack import com.ramcosta.composedestinations.utils.startDestination import com.imashnake.animite.R as Res @@ -52,12 +53,17 @@ fun NavigationBar( ) { WindowInsets.displayCutout } else { WindowInsets(0.dp) } ) { val currentDestination by navController.currentDestinationAsState() - - NavigationBarPaths.values().forEach { destination -> + NavigationBarPaths.entries.forEach { destination -> + val isCurrentDestOnBackStack = navController.isRouteOnBackStack(destination.route) NavigationBarItem( modifier = Modifier.navigationBarsPadding(), selected = currentDestination?.startDestination == destination.route, onClick = { + if (isCurrentDestOnBackStack) { + navController.popBackStack(destination.route.route, false) + return@NavigationBarItem + } + navController.navigate(destination.route.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true diff --git a/app/src/main/java/com/imashnake/animite/features/searchbar/SearchViewModel.kt b/app/src/main/java/com/imashnake/animite/features/searchbar/SearchViewModel.kt index b328cf7b..0a77f773 100644 --- a/app/src/main/java/com/imashnake/animite/features/searchbar/SearchViewModel.kt +++ b/app/src/main/java/com/imashnake/animite/features/searchbar/SearchViewModel.kt @@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.imashnake.animite.api.anilist.AnilistSearchRepository import com.imashnake.animite.api.anilist.type.MediaType -import com.imashnake.animite.data.Resource -import com.imashnake.animite.data.Resource.Companion.asResource +import com.imashnake.animite.core.data.Resource +import com.imashnake.animite.core.data.Resource.Companion.asResource import com.imashnake.animite.dev.internal.Constants import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f5ccf047..f841b1b4 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin) diff --git a/app/src/main/java/com/imashnake/animite/data/Resource.kt b/core/src/main/kotlin/com/imashnake/animite/core/data/Resource.kt similarity index 97% rename from app/src/main/java/com/imashnake/animite/data/Resource.kt rename to core/src/main/kotlin/com/imashnake/animite/core/data/Resource.kt index ff9e8c00..784cbbeb 100644 --- a/app/src/main/java/com/imashnake/animite/data/Resource.kt +++ b/core/src/main/kotlin/com/imashnake/animite/core/data/Resource.kt @@ -1,4 +1,4 @@ -package com.imashnake.animite.data +package com.imashnake.animite.core.data import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow diff --git a/core/src/main/res/drawable/ic_anilist.xml b/core/src/main/res/drawable/ic_anilist.xml new file mode 100644 index 00000000..aa99a100 --- /dev/null +++ b/core/src/main/res/drawable/ic_anilist.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f9d6c6f..e9d6045d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,10 @@ espresso = "3.6.0-alpha02" # https://github.com/apollographql/apollo-kotlin/releases. apollo = "3.8.2" +# DataStore +# https://developer.android.com/jetpack/androidx/releases/datastore +datastore = "1.0.0" + # COIL # https://github.com/coil-kt/coil/blob/main/CHANGELOG.md. coil = "2.5.0" @@ -102,6 +106,7 @@ apollo-cache-sqlite = { group = "com.apollographql.apollo3", name = "apollo-norm coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } kotlin-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "dagger" } hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "dagger" } hilt-navigationCompose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt" } diff --git a/material-color-utilities/build.gradle.kts b/material-color-utilities/build.gradle.kts index 2c585ff0..4b0c9932 100644 --- a/material-color-utilities/build.gradle.kts +++ b/material-color-utilities/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin) diff --git a/profile/build.gradle.kts b/profile/build.gradle.kts index b0ccc33a..dc8acdf7 100644 --- a/profile/build.gradle.kts +++ b/profile/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin) @@ -40,6 +38,9 @@ ksp { dependencies { implementation(project(":core")) + implementation(project(":api:anilist")) + implementation(project(":api:preferences")) + // AndroidX implementation(libs.androidx.activityCompose) implementation(libs.androidx.coreKtx) @@ -59,6 +60,11 @@ dependencies { implementation(libs.kotlin.coroutines.android) implementation(libs.kotlin.coroutines.core) + // Hilt + implementation(libs.hilt.android) + implementation(libs.hilt.navigationCompose) + ksp(libs.hilt.android.compiler) + // Compose Destinations implementation(libs.compose.destinations) ksp(libs.compose.destinations.ksp) diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/Login.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/Login.kt new file mode 100644 index 00000000..42527629 --- /dev/null +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/Login.kt @@ -0,0 +1,49 @@ +package com.imashnake.animite.profile + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.imashnake.animite.core.ui.LocalPaddings +import com.imashnake.animite.profile.dev.internal.ANILIST_AUTH_URL +import com.imashnake.animite.core.R as coreR + +@Composable +fun Login(modifier: Modifier = Modifier) { + val uriHandler = LocalUriHandler.current + OutlinedButton( + onClick = { uriHandler.openUri(ANILIST_AUTH_URL) }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1E2630), + contentColor = Color.White + ), + modifier = modifier + ) { + Row(Modifier.wrapContentWidth()) { + Icon( + imageVector = ImageVector.vectorResource(coreR.drawable.ic_anilist), + contentDescription = "AniList icon", + tint = Color.Unspecified, + modifier = Modifier.size(24.dp) + ) + Text( + text = "Log in", + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = LocalPaddings.current.tiny) + ) + } + } +} diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt index 749cbcc5..b0ad65f0 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt @@ -1,44 +1,66 @@ package com.imashnake.animite.profile import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.imashnake.animite.core.R as coreR -import com.imashnake.animite.core.ui.LocalPaddings -import com.imashnake.animite.core.ui.ProgressIndicator +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.hilt.navigation.compose.hiltViewModel +import com.imashnake.animite.profile.dev.internal.ANILIST_AUTH_DEEPLINK +import com.ramcosta.composedestinations.annotation.DeepLink import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph -@Destination(route = "profile-screen") +@Destination( + route = "user", + deepLinks = [ + DeepLink( + uriPattern = ANILIST_AUTH_DEEPLINK + ) + ] +) @RootNavGraph(start = true) @Composable -fun ProfileScreen() { +fun ProfileScreen( + viewModel: ProfileViewModel = hiltViewModel(), + accessToken: String? = null +) { + accessToken?.let { viewModel.setAccessToken(it) } + val isLoggedIn by viewModel.isLoggedIn.collectAsState(initial = false) + val viewer by viewModel.viewer.collectAsState() + Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy( - LocalPaddings.current.tiny - ) - ) { - Text( - text = stringResource(coreR.string.coming_soon), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.labelLarge - ) - ProgressIndicator() + if (!isLoggedIn) { + Login() + } else { + viewer.data?.let { + Text(text = buildAnnotatedString { + append("Logged in as ") + withStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + ) { + append(it.name) + } + append(" \uD83D\uDE33") + }) + } } } } diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt new file mode 100644 index 00000000..026a5298 --- /dev/null +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt @@ -0,0 +1,34 @@ +package com.imashnake.animite.profile + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.imashnake.animite.api.anilist.AnilistUserRepository +import com.imashnake.animite.api.preferences.PreferencesRepository +import com.imashnake.animite.core.data.Resource +import com.imashnake.animite.core.data.Resource.Companion.asResource +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProfileViewModel @Inject constructor( + userRepository: AnilistUserRepository, + private val preferencesRepository: PreferencesRepository +) : ViewModel() { + fun setAccessToken(accessToken: String?) = viewModelScope.launch(Dispatchers.IO) { + preferencesRepository.setAccessToken(accessToken) + } + + val isLoggedIn = preferencesRepository + .accessToken + .map { !it.isNullOrEmpty() } + + val viewer = userRepository + .fetchViewer() + .asResource() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(1000), Resource.loading()) +} diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/dev/internal/Constants.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/dev/internal/Constants.kt new file mode 100644 index 00000000..69e81706 --- /dev/null +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/dev/internal/Constants.kt @@ -0,0 +1,4 @@ +package com.imashnake.animite.profile.dev.internal + +const val ANILIST_AUTH_DEEPLINK = "jinnah://animite#access_token={accessToken}&token_type=Bearer&expires_in=31622400" +const val ANILIST_AUTH_URL = "https://anilist.co/api/v2/oauth/authorize?client_id=10678&response_type=token" diff --git a/rslash/build.gradle.kts b/rslash/build.gradle.kts index 7a8a10b8..f5626350 100644 --- a/rslash/build.gradle.kts +++ b/rslash/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin) diff --git a/settings.gradle.kts b/settings.gradle.kts index b65f64bf..fae925ea 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ plugins { rootProject.name = "Animite" include( ":api:anilist", + ":api:preferences", ":material-color-utilities", ":core", ":profile",