From 8e674735981e4ee5a8d844335383fead93abcd43 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Tue, 3 Mar 2026 23:38:27 +0000 Subject: [PATCH 01/10] Replace Nav2 with Nav3: Part 1 * Each screen is now internal & provided by hilt (can also be done with koin so still KMP compliant-ish for now) * NavigationBarPaths was naughty and had too many responsibilities * All navigation is now provided by the Navigator class, similar to the nav3 class, but it's just a wrapper around a statelist * Deep links currently broken because they have to be parsed manually in nav3 * the nav stack is now flattened, so removed a lot of logic with destinations, nested graphs, etc --- .../imashnake/animite/anime/AnimeViewModel.kt | 9 +- .../anime/navigation/di/NavigationModule.kt | 33 ++++ app/build.gradle.kts | 6 +- .../imashnake/animite/ApplicationModule.kt | 14 ++ .../animite/features/MainActivity.kt | 160 +++++++----------- gradle/libs.versions.toml | 16 +- .../imashnake/animite/manga/MangaViewModel.kt | 11 -- .../manga/navigation/di/NavigationModule.kt | 33 ++++ .../com/imashnake/animite/media/MediaPage.kt | 53 ++++-- .../animite/media/MediaPageViewModel.kt | 30 ++-- .../media/navigation/di/NavigationModule.kt | 39 +++++ navigation/build.gradle.kts | 19 ++- .../animite/navigation/NavigationBar.kt | 28 +-- .../animite/navigation/NavigationBarPaths.kt | 90 ++-------- .../animite/navigation/NavigationRail.kt | 41 ++--- .../imashnake/animite/navigation/Navigator.kt | 27 +++ .../navigation/TopLevelDestinations.kt | 9 +- .../animite/navigation/di/NavigationModule.kt | 24 +++ profile/build.gradle.kts | 8 +- .../animite/profile/ProfileScreen.kt | 11 +- .../animite/profile/ProfileViewModel.kt | 18 +- .../profile/navigation/di/NavigationModule.kt | 37 ++++ settings/build.gradle.kts | 8 +- .../animite/settings/SettingsPage.kt | 116 ++++++++----- .../navigation/di/NavigationModule.kt | 28 +++ social/build.gradle.kts | 12 +- .../social/navigation/di/NavigationModule.kt | 26 +++ 27 files changed, 579 insertions(+), 327 deletions(-) create mode 100644 anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt create mode 100644 app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt create mode 100644 manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt create mode 100644 media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt create mode 100644 navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt create mode 100644 navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt create mode 100644 profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt create mode 100644 settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt create mode 100644 social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt diff --git a/anime/src/main/kotlin/com/imashnake/animite/anime/AnimeViewModel.kt b/anime/src/main/kotlin/com/imashnake/animite/anime/AnimeViewModel.kt index cb401eeb1..95550fdb3 100644 --- a/anime/src/main/kotlin/com/imashnake/animite/anime/AnimeViewModel.kt +++ b/anime/src/main/kotlin/com/imashnake/animite/anime/AnimeViewModel.kt @@ -1,6 +1,5 @@ package com.imashnake.animite.anime -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.imashnake.animite.api.anilist.AnilistMediaRepository @@ -20,6 +19,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -30,11 +30,10 @@ import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) class AnimeViewModel @Inject constructor( private val mediaListRepository: AnilistMediaRepository, - private val savedStateHandle: SavedStateHandle, preferencesRepository: PreferencesRepository ) : ViewModel() { - private val now = savedStateHandle.getStateFlow(NOW, LocalDate.now()) + private val now = flowOf(LocalDate.now()) private val refreshTrigger = MutableSharedFlow() val dayHour = preferencesRepository.dayHour @@ -126,8 +125,4 @@ class AnimeViewModel @Inject constructor( useNetwork = false setIsRefreshing(false) } - - companion object { - const val NOW = "now" - } } diff --git a/anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt b/anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt new file mode 100644 index 000000000..cd956c7bd --- /dev/null +++ b/anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt @@ -0,0 +1,33 @@ +package com.imashnake.animite.anime.navigation.di + +import androidx.compose.animation.SharedTransitionScope +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.ui.LocalNavAnimatedContentScope +import com.imashnake.animite.anime.AnimeScreen +import com.imashnake.animite.navigation.AnimeRoute +import com.imashnake.animite.navigation.Navigator +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +@Module +@InstallIn(ActivityRetainedComponent::class) +object NavigationModule { + + @Provides + @IntoSet + fun provideNavEntry( + navigator: Navigator, + ): EntryProviderScope.(SharedTransitionScope) -> Unit = { sharedScope -> + entry { + AnimeScreen( + onNavigateToMediaItem = navigator::navigate, + sharedTransitionScope = sharedScope, + animatedVisibilityScope = LocalNavAnimatedContentScope.current, + ) + } + } +} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ae3d42339..aca7a0ab0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,10 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) signingConfig = signingConfigs.getByName( if (System.getenv("Animite") == "true") "release" else "debug" ) @@ -81,7 +84,6 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.lifecycleRuntimeKtx) - implementation(libs.androidx.navigationCompose) // Compose implementation(libs.bundles.compose) diff --git a/app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt b/app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt new file mode 100644 index 000000000..9a4df9c8f --- /dev/null +++ b/app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt @@ -0,0 +1,14 @@ +package com.imashnake.animite + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object ApplicationModule { + + @Provides + fun provideVersionName() = BuildConfig.VERSION_NAME +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt index f0af2c464..add43cc10 100644 --- a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt +++ b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt @@ -1,10 +1,7 @@ package com.imashnake.animite.features -import android.content.Intent import android.content.res.Configuration -import android.os.Build import android.os.Bundle -import android.view.RoundedCorner import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -26,43 +23,42 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navDeepLink -import com.imashnake.animite.BuildConfig -import com.imashnake.animite.anime.AnimeScreen +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay import com.imashnake.animite.api.anilist.sanitize.media.MediaList import com.imashnake.animite.core.ui.LocalPaddings import com.imashnake.animite.features.searchbar.SearchFrontDrop import com.imashnake.animite.features.theme.AnimiteTheme -import com.imashnake.animite.manga.MangaScreen import com.imashnake.animite.media.MediaPage -import com.imashnake.animite.navigation.AnimeRoute -import com.imashnake.animite.navigation.MangaRoute import com.imashnake.animite.navigation.NavigationBar import com.imashnake.animite.navigation.NavigationBarPaths import com.imashnake.animite.navigation.NavigationRail -import com.imashnake.animite.navigation.ProfileRoute -import com.imashnake.animite.navigation.SocialRoute -import com.imashnake.animite.profile.ProfileScreen -import com.imashnake.animite.profile.dev.internal.ANILIST_AUTH_DEEPLINK +import com.imashnake.animite.navigation.Navigator +import com.imashnake.animite.navigation.di.EntryInstaller import com.imashnake.animite.settings.SettingsPage import com.imashnake.animite.settings.SettingsViewModel import com.imashnake.animite.settings.Theme -import com.imashnake.animite.social.SocialScreen import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.filterNotNull +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { + + @Inject + internal lateinit var entryProviders: Set<@JvmSuppressWildcards EntryInstaller> + + @Inject + internal lateinit var navigator: Navigator + private val settingsViewModel: SettingsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -78,7 +74,7 @@ class MainActivity : ComponentActivity() { .filterNotNull() .collectAsState(initial = false) - val useDarkTheme = when(Theme.valueOf(theme)) { + val useDarkTheme = when (Theme.valueOf(theme)) { Theme.DARK -> true Theme.LIGHT -> false Theme.DEVICE_THEME -> isSystemInDarkTheme() @@ -92,8 +88,8 @@ class MainActivity : ComponentActivity() { dayHour = dayHour ) { MainScreen( - deviceScreenCornerRadius = getDeviceScreenCornerRadius(), - useDarkTheme = useDarkTheme, + navigator = navigator, + navEntries = entryProviders, modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) @@ -101,36 +97,19 @@ class MainActivity : ComponentActivity() { } } } - - private fun getDeviceScreenCornerRadius(): Int { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return windowManager - .currentWindowMetrics - .windowInsets - .getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT) - ?.radius - ?: 0 - } - return 0 - } } @OptIn(ExperimentalLayoutApi::class) @Composable fun MainScreen( - deviceScreenCornerRadius: Int, - useDarkTheme: Boolean, + navigator: Navigator, + navEntries: Set, modifier: Modifier = Modifier, ) { - val navController = rememberNavController() - - val currentBackStackEntry by navController.currentBackStackEntryAsState() - val isNavBarVisible = rememberSaveable(currentBackStackEntry) { - if (currentBackStackEntry != null) { - NavigationBarPaths.entries.any { - it.matchesDestination(currentBackStackEntry!!) - } - } else false + val isNavBarVisible by remember(navigator.backStack) { + derivedStateOf { + NavigationBarPaths.entries.any { it.route == navigator.backStack.lastOrNull() } + } } // TODO: Refactor to use Scaffold once AnimatedVisibility issues are fixed; @@ -140,72 +119,48 @@ fun MainScreen( LocalContentColor provides MaterialTheme.colorScheme.onBackground ) { SharedTransitionLayout { - NavHost(navController = navController, startDestination = AnimeRoute) { - composable { - AnimeScreen( - onNavigateToMediaItem = navController::navigate, - sharedTransitionScope = this@SharedTransitionLayout, - animatedVisibilityScope = this, - ) - } - composable { - MangaScreen( - onNavigateToMediaItem = navController::navigate, - sharedTransitionScope = this@SharedTransitionLayout, - animatedVisibilityScope = this, - ) - } - composable { - MediaPage( - onBack = navController::navigateUp, - onNavigateToMediaItem = navController::navigate, - useDarkTheme = useDarkTheme, - deviceScreenCornerRadius = deviceScreenCornerRadius, - sharedTransitionScope = this@SharedTransitionLayout, - animatedVisibilityScope = this, - ) + NavDisplay( + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ), + backStack = navigator.backStack, + entryProvider = entryProvider { + navEntries.forEach { builder -> + builder(this@SharedTransitionLayout) + } } - composable( - deepLinks = listOf( - navDeepLink { - uriPattern = ANILIST_AUTH_DEEPLINK - action = Intent.ACTION_VIEW - } - ) - ) { - ProfileScreen( - onNavigateToMediaItem = navController::navigate, - onNavigateToSettings = navController::navigate, - sharedTransitionScope = this@SharedTransitionLayout, - animatedVisibilityScope = this, - ) - } - composable { - SettingsPage(versionName = BuildConfig.VERSION_NAME) - } - composable { - SocialScreen() - } - } + ) } } - when(LocalConfiguration.current.orientation) { + when (LocalConfiguration.current.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { AnimatedVisibility( visible = isNavBarVisible, modifier = Modifier.align(Alignment.CenterStart), enter = slideInHorizontally { -it }, exit = slideOutHorizontally { -it } - ) { NavigationRail(navController = navController) } + ) { + NavigationRail( + backStack = navigator.backStack, + onNavigate = navigator::navigate, + ) + } } + else -> { AnimatedVisibility( visible = isNavBarVisible, modifier = Modifier.align(Alignment.BottomCenter), enter = slideInVertically { it }, exit = slideOutVertically { it } - ) { NavigationBar(navController = navController) } + ) { + NavigationBar( + backStack = navigator.backStack, + onNavigate = navigator::navigate, + ) + } } } @@ -213,16 +168,15 @@ fun MainScreen( hasExtraPadding = isNavBarVisible && LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT, onItemClick = { id, mediaType, title -> - navController.navigate( - MediaPage( - id = id, - source = MediaList.Type.SEARCH.name, - mediaType = mediaType.rawValue, - title = title, - ) + val route = MediaPage( + id = id, + source = MediaList.Type.SEARCH.name, + mediaType = mediaType.rawValue, + title = title, ) + navigator.navigate(route) }, - isFabVisible = currentBackStackEntry?.destination?.hasRoute() == false, + isFabVisible = !navigator.backStack.contains(SettingsPage), modifier = Modifier .align(Alignment.BottomEnd) .padding( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3e752730..5985d1556 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,6 @@ coreSplashscreen = "1.2.0" lifecycle = "2.10.0" extJunit = "1.3.0" espresso = "3.7.0" -navigation = "2.9.7" # TODO: Breaking changes; review release notes and update. # Apollo Kotlin @@ -104,6 +103,10 @@ materialKolor = "4.1.1" # https://github.com/saket/cascade/releases saketCascade = "2.3.0" +# Nav3 +nav3Core = "1.1.0-alpha05" +lifecycleViewmodelNav3 = "2.10.0" + [libraries] android-desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugaring" } @@ -111,8 +114,6 @@ androidx-activityCompose = { group = "androidx.activity", name = "activity-compo androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } androidx-lifecycleRuntimeKtx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } -androidx-navigationCommon = { group = "androidx.navigation", name = "navigation-common", version.ref = "navigation" } -androidx-navigationCompose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "extJunit" } androidx-test-espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" } compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "composeAnimation" } @@ -155,6 +156,9 @@ ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-contentNegotation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } saket-cascade = { group = "me.saket.cascade", name = "cascade-compose", version.ref = "saketCascade" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } +androidx-navigation3-vm = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" } [bundles] @@ -179,3 +183,9 @@ ktor = [ "ktor-contentNegotation", "ktor-serialization" ] + +nav3 = [ + "androidx-navigation3-runtime", + "androidx-navigation3-ui", + "androidx-navigation3-vm", +] diff --git a/manga/src/main/kotlin/com/imashnake/animite/manga/MangaViewModel.kt b/manga/src/main/kotlin/com/imashnake/animite/manga/MangaViewModel.kt index 1e0af5ddf..7ff79b26d 100644 --- a/manga/src/main/kotlin/com/imashnake/animite/manga/MangaViewModel.kt +++ b/manga/src/main/kotlin/com/imashnake/animite/manga/MangaViewModel.kt @@ -1,6 +1,5 @@ package com.imashnake.animite.manga -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.imashnake.animite.api.anilist.AnilistMediaRepository @@ -10,31 +9,25 @@ import com.imashnake.animite.api.anilist.type.MediaType import com.imashnake.animite.api.preferences.PreferencesRepository import com.imashnake.animite.core.data.Resource import com.imashnake.animite.core.data.Resource.Companion.asResource -import com.imashnake.animite.media.ext.nextSeason -import com.imashnake.animite.media.ext.season import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import java.time.LocalDate import javax.inject.Inject @HiltViewModel @OptIn(ExperimentalCoroutinesApi::class) class MangaViewModel @Inject constructor( private val mediaListRepository: AnilistMediaRepository, - private val savedStateHandle: SavedStateHandle, preferencesRepository: PreferencesRepository ) : ViewModel() { - private val now = savedStateHandle.getStateFlow(NOW, LocalDate.now()) private val refreshTrigger = MutableSharedFlow() val dayHour = preferencesRepository.dayHour @@ -88,8 +81,4 @@ class MangaViewModel @Inject constructor( useNetwork = false setIsRefreshing(false) } - - companion object { - const val NOW = "now" - } } diff --git a/manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt b/manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt new file mode 100644 index 000000000..3984fece7 --- /dev/null +++ b/manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt @@ -0,0 +1,33 @@ +package com.imashnake.animite.manga.navigation.di + +import androidx.compose.animation.SharedTransitionScope +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.ui.LocalNavAnimatedContentScope +import com.imashnake.animite.manga.MangaScreen +import com.imashnake.animite.navigation.MangaRoute +import com.imashnake.animite.navigation.Navigator +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +@Module +@InstallIn(ActivityRetainedComponent::class) +object NavigationModule { + + @Provides + @IntoSet + fun provideNavEntry( + navigator: Navigator + ): EntryProviderScope.(SharedTransitionScope) -> Unit = { sharedScope -> + entry { + MangaScreen( + onNavigateToMediaItem = navigator::navigate, + sharedTransitionScope = sharedScope, + animatedVisibilityScope = LocalNavAnimatedContentScope.current, + ) + } + } +} \ No newline at end of file diff --git a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt index 6d59d5cf8..40b59a48f 100644 --- a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt +++ b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt @@ -3,6 +3,8 @@ package com.imashnake.animite.media import android.content.Intent +import android.os.Build +import android.view.RoundedCorner import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility @@ -96,6 +98,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -113,7 +116,7 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.lerp import androidx.compose.ui.zIndex import androidx.core.net.toUri -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.NavKey import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade @@ -160,12 +163,12 @@ private const val RELATIONS = "Relations" fun MediaPage( onBack: () -> Unit, onNavigateToMediaItem: (MediaPage) -> Unit, - deviceScreenCornerRadius: Int, useDarkTheme: Boolean, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, + viewModel: MediaPageViewModel, + deviceScreenCornerRadius: Int = getTopRightRadius(), contentWindowInsets: WindowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout), - viewModel: MediaPageViewModel = hiltViewModel(), ) { val insetPaddingValues = contentWindowInsets.asPaddingValues() val horizontalInsets = insetPaddingValues.horizontalOnly @@ -474,8 +477,8 @@ fun MediaPage( .padding(bottom = LocalPaddings.current.large) .graphicsLayer { val pageOffset = ( - characterPagerState.currentPage - page + characterPagerState.currentPageOffsetFraction - ).absoluteValue + characterPagerState.currentPage - page + characterPagerState.currentPageOffsetFraction + ).absoluteValue alpha = lerp( start = 0f, @@ -547,8 +550,11 @@ fun MediaPage( MediaDescription( html = description, onLinkClick = onLinkClick@{ - val id = it?.split("/")?.getOrNull(4)?.toIntOrNull() ?: return@onLinkClick null - val character = media.characters?.find { character -> character.id == id } ?: return@onLinkClick null + val id = it?.split("/")?.getOrNull(4)?.toIntOrNull() + ?: return@onLinkClick null + val character = + media.characters?.find { character -> character.id == id } + ?: return@onLinkClick null val index = media.characters.indexOf(character) if (index != -1) { coroutineScope.launch { @@ -563,8 +569,8 @@ fun MediaPage( .padding(top = LocalPaddings.current.medium) .graphicsLayer { val pageOffset = ( - characterPagerState.currentPage - page + characterPagerState.currentPageOffsetFraction - ).absoluteValue + characterPagerState.currentPage - page + characterPagerState.currentPageOffsetFraction + ).absoluteValue alpha = lerp( start = 0f, @@ -678,7 +684,8 @@ fun MediaPage( ) { if (it) { MediaMediumGrid( - mediaMediumList = media.genreTitleList?.second ?: persistentListOf(), + mediaMediumList = media.genreTitleList?.second + ?: persistentListOf(), onItemClick = { id, title -> onNavigateToMediaItem( MediaPage( @@ -692,7 +699,8 @@ fun MediaPage( ) } else { MediaMediumList( - mediaMediumList = media.genreTitleList?.second ?: persistentListOf(), + mediaMediumList = media.genreTitleList?.second + ?: persistentListOf(), onItemClick = { id, title -> onNavigateToMediaItem( MediaPage( @@ -879,6 +887,7 @@ private fun MediaInfo( ) ) } + else -> { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -896,13 +905,14 @@ private fun MediaInfo( style = MaterialTheme.typography.labelSmallEmphasized ) Text( - text = when(it) { + text = when (it) { is Media.Info.Item -> it.value is Media.Info.Season -> listOfNotNull( stringResource(it.season.res), it.year ).joinToString(" ") + else -> stringResource( - when(it) { + when (it) { is Media.Info.Format -> it.format.res is Media.Info.Status -> it.status.res is Media.Info.Source -> it.source.res @@ -1235,10 +1245,25 @@ private fun MediaRecommendations( } } +@Composable +private fun getTopRightRadius(): Int { + val view = LocalView.current + // Use remember to avoid recalculating on every recomposition + // but observe the view's layout/insets if necessary. + return remember(view) { + val insets = view.rootWindowInsets + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + insets?.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.radius ?: 0 + } else { + 0 + } + } +} + @Serializable data class MediaPage( val id: Int, val source: String, val mediaType: String, val title: String?, -) +) : NavKey diff --git a/media/src/main/kotlin/com/imashnake/animite/media/MediaPageViewModel.kt b/media/src/main/kotlin/com/imashnake/animite/media/MediaPageViewModel.kt index 510715afd..f048baa41 100644 --- a/media/src/main/kotlin/com/imashnake/animite/media/MediaPageViewModel.kt +++ b/media/src/main/kotlin/com/imashnake/animite/media/MediaPageViewModel.kt @@ -3,33 +3,35 @@ package com.imashnake.animite.media import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute import com.imashnake.animite.api.anilist.AnilistMediaRepository import com.imashnake.animite.api.anilist.type.MediaSort import com.imashnake.animite.api.anilist.type.MediaType +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import java.io.IOException -import javax.inject.Inject -@HiltViewModel +@HiltViewModel(assistedFactory = MediaPageViewModel.Factory::class) @Suppress("SwallowedException") -class MediaPageViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class MediaPageViewModel @AssistedInject constructor( + @Assisted navArgs: MediaPage, private val mediaRepository: AnilistMediaRepository ) : ViewModel() { - private val navArgs = savedStateHandle.toRoute() - var uiState by mutableStateOf(MediaUiState( + + var uiState by mutableStateOf( + MediaUiState( source = navArgs.source, id = navArgs.id, type = navArgs.mediaType, title = navArgs.title - )) + ) + ) private set init { @@ -58,7 +60,7 @@ class MediaPageViewModel @Inject constructor( relations = media?.relations, recommendations = media?.recommendations ) - } catch(_: IOException) { + } catch (_: IOException) { TODO() } } @@ -66,7 +68,8 @@ class MediaPageViewModel @Inject constructor( fun getGenreMediaMediums(genre: String?) = viewModelScope.launch { if (genre == null) { - uiState = uiState.copy(genreTitleList = uiState.genreTitleList?.first.orEmpty() to persistentListOf()) + uiState = + uiState.copy(genreTitleList = uiState.genreTitleList?.first.orEmpty() to persistentListOf()) return@launch } val list = mediaRepository.fetchMediaMediumList( @@ -79,4 +82,9 @@ class MediaPageViewModel @Inject constructor( uiState = uiState.copy(genreTitleList = list?.let { genre to it }) } + + @AssistedFactory + interface Factory { + fun create(navArgs: MediaPage): MediaPageViewModel + } } diff --git a/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt b/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt new file mode 100644 index 000000000..e360fda30 --- /dev/null +++ b/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt @@ -0,0 +1,39 @@ +package com.imashnake.animite.media.navigation.di + +import androidx.compose.animation.SharedTransitionScope +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.ui.LocalNavAnimatedContentScope +import com.imashnake.animite.media.MediaPage +import com.imashnake.animite.media.MediaPageViewModel +import com.imashnake.animite.navigation.Navigator +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +@Module +@InstallIn(ActivityRetainedComponent::class) +object NavigationModule { + + @Provides + @IntoSet + fun provideNavEntry( + navigator: Navigator, + ): EntryProviderScope.(SharedTransitionScope) -> Unit = { sharedScope -> + entry { args -> + MediaPage( + onBack = navigator::popBack, + onNavigateToMediaItem = navigator::navigate, + useDarkTheme = true, // fix? + sharedTransitionScope = sharedScope, + animatedVisibilityScope = LocalNavAnimatedContentScope.current, + viewModel = hiltViewModel { factory -> + factory.create(args) + } + ) + } + } +} \ No newline at end of file diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts index 0a4146456..05deba16f 100644 --- a/navigation/build.gradle.kts +++ b/navigation/build.gradle.kts @@ -3,13 +3,18 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.compose.compiler) alias(libs.plugins.detekt) + alias(libs.plugins.hilt) + alias(libs.plugins.ksp) } android { buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } @@ -23,10 +28,18 @@ kotlin { } dependencies { - api(libs.androidx.navigationCompose) + api(libs.bundles.nav3) + + // Hilt + implementation(libs.hilt.android) + implementation(libs.hilt.navigationCompose) + ksp(libs.hilt.android.compiler) + implementation(libs.bundles.compose) implementation(libs.compose.material) - debugImplementation(libs.compose.ui.tooling) implementation(libs.compose.ui.toolingPreview) + implementation(libs.kotlinx.serialization.core) + + debugImplementation(libs.compose.ui.tooling) } diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt index 2d5cd5dc3..7484a8748 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt @@ -8,34 +8,34 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Surface import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember 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.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation3.runtime.NavKey @Composable fun NavigationBar( - navController: NavController, + backStack: List, + onNavigate: (NavKey) -> Unit, modifier: Modifier = Modifier, containerColor: Color = NavigationBarDefaults.containerColor, contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), tonalElevation: Dp = NavigationBarDefaults.Elevation, windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, ) { - val currentBackStackEntry by navController.currentBackStackEntryAsState() - // This is a clone of Material3 NavigationBar, except we've shrunk the height from 80dp to 65dp Surface( color = containerColor, @@ -53,13 +53,15 @@ fun NavigationBar( verticalAlignment = Alignment.CenterVertically, ) { NavigationBarPaths.entries.forEach { destination -> - val selected = remember(destination, currentBackStackEntry) { - currentBackStackEntry?.let { destination.matchesDestination(it) } == true - } NavigationBarItem( - selected = selected, - onClick = { if (!selected) destination.navigateTo(navController) }, - icon = destination.icon, + selected = backStack.contains(destination.route), + onClick = { onNavigate(destination.route) }, + icon = { + Icon( + ImageVector.vectorResource(destination.icon), + contentDescription = stringResource(destination.iconDescription) + ) + }, modifier = Modifier.height(dimensionResource(R.dimen.navigation_bar_height)), ) } diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt index 559f6acdf..0ab099c25 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt @@ -1,91 +1,35 @@ package com.imashnake.animite.navigation +import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation3.runtime.NavKey enum class NavigationBarPaths( - val navigateTo: (NavController) -> Unit, - val matchesDestination: (NavBackStackEntry) -> Boolean, - val icon: @Composable () -> Unit, - @param:StringRes val labelRes: Int + val route: NavKey, + @param:DrawableRes val icon: Int, + @param:StringRes val iconDescription: Int, ) { Social( - navigateTo = { - it.navigate(SocialRoute) { - popUpTo(id = it.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - } - }, - matchesDestination = { navBackStackEntry -> - navBackStackEntry.destination.hierarchy.any { it.hasRoute(SocialRoute::class) } - }, - icon = { - Icon(ImageVector.vectorResource(R.drawable.social), contentDescription = stringResource(R.string.social)) - }, - labelRes = R.string.social + route = SocialRoute, + icon = R.drawable.social, + iconDescription = R.string.social, ), Anime( - navigateTo = { - it.navigate(AnimeRoute) { - popUpTo(id = it.graph.findStartDestination().id) { - saveState = true - inclusive = true - } - launchSingleTop = true - } - }, - matchesDestination = { navBackStackEntry -> - navBackStackEntry.destination.hierarchy.any { it.hasRoute(AnimeRoute::class) } - }, - icon = { - Icon(ImageVector.vectorResource(R.drawable.anime), contentDescription = stringResource(R.string.anime)) - }, - labelRes = R.string.anime + route = AnimeRoute, + icon = R.drawable.anime, + iconDescription = R.string.anime, ), Manga( - navigateTo = { - it.navigate(MangaRoute) { - popUpTo(id = it.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - } - }, - matchesDestination = { navBackStackEntry -> - navBackStackEntry.destination.hierarchy.any { it.hasRoute(MangaRoute::class) } - }, - icon = { - Icon(ImageVector.vectorResource(R.drawable.manga), contentDescription = stringResource(R.string.manga)) - }, - labelRes = R.string.manga + route = MangaRoute, + icon = R.drawable.manga, + iconDescription = R.string.manga, ), - Profile( - navigateTo = { - it.navigate(ProfileRoute()) { - popUpTo(id = it.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - } - }, - matchesDestination = { navBackStackEntry -> - navBackStackEntry.destination.hierarchy.any { it.hasRoute(ProfileRoute::class) } - }, - icon = { - Icon(ImageVector.vectorResource(R.drawable.profile), contentDescription = stringResource(R.string.profile)) - }, - labelRes = R.string.profile + route = ProfileRoute(), + icon = R.drawable.profile, + iconDescription = R.string.profile, ), } diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt index 3d8742908..7ee1c963a 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationRailDefaults @@ -22,28 +23,28 @@ import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Surface import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember 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.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation3.runtime.NavKey @Composable fun NavigationRail( - navController: NavController, + backStack: List, + onNavigate: (NavKey) -> Unit, modifier: Modifier = Modifier, containerColor: Color = NavigationBarDefaults.containerColor, contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), tonalElevation: Dp = NavigationBarDefaults.Elevation, windowInsets: WindowInsets = NavigationRailDefaults.windowInsets.union(WindowInsets.displayCutout), ) { - val currentBackStackEntry by navController.currentBackStackEntryAsState() val insetPaddingValues = windowInsets.asPaddingValues() val layoutDirection = LocalLayoutDirection.current @@ -56,24 +57,26 @@ fun NavigationRail( modifier = modifier, ) { Column( - modifier = Modifier - .fillMaxHeight() - .padding(start = insetPaddingValues.calculateStartPadding(layoutDirection)) - .windowInsetsPadding(windowInsets.only(WindowInsetsSides.Vertical)) - .defaultMinSize(minWidth = dimensionResource(R.dimen.navigation_rail_width)) - .padding(vertical = 4.dp) - .selectableGroup(), + modifier = Modifier + .fillMaxHeight() + .padding(start = insetPaddingValues.calculateStartPadding(layoutDirection)) + .windowInsetsPadding(windowInsets.only(WindowInsetsSides.Vertical)) + .defaultMinSize(minWidth = dimensionResource(R.dimen.navigation_rail_width)) + .padding(vertical = 4.dp) + .selectableGroup(), verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { NavigationBarPaths.entries.forEach { destination -> - val selected = remember(destination, currentBackStackEntry) { - currentBackStackEntry?.let { destination.matchesDestination(it) } == true - } NavigationRailItem( - selected = selected, - onClick = { if (!selected) destination.navigateTo(navController) }, - icon = destination.icon, + selected = backStack.contains(destination.route), + onClick = { onNavigate(destination.route) }, + icon = { + Icon( + ImageVector.vectorResource(destination.icon), + contentDescription = stringResource(destination.iconDescription) + ) + }, modifier = Modifier.width(dimensionResource(R.dimen.navigation_rail_width)) ) } diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt new file mode 100644 index 000000000..c0b9f11fe --- /dev/null +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt @@ -0,0 +1,27 @@ +package com.imashnake.animite.navigation + +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import dagger.hilt.android.scopes.ActivityRetainedScoped + +@ActivityRetainedScoped +class Navigator( + startDestination: NavKey, +) { + + val backStack = NavBackStack(startDestination) + + fun navigate(navKey: NavKey, allowMultiple: Boolean = false) { + if (!allowMultiple && backStack.contains(navKey)) return + + if (NavigationBarPaths.entries.any { it.route == navKey }) { + backStack.clear() + } + + backStack.add(navKey) + } + + fun popBack() { + backStack.removeLastOrNull() + } +} \ No newline at end of file diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/TopLevelDestinations.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/TopLevelDestinations.kt index 1d1153fff..562839560 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/TopLevelDestinations.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/TopLevelDestinations.kt @@ -1,5 +1,6 @@ package com.imashnake.animite.navigation +import androidx.navigation3.runtime.NavKey import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -11,13 +12,13 @@ data class ProfileRoute( val tokenType: String? = null, @SerialName("expiresIn") val expiresIn: Int = -1 -) +) : NavKey @Serializable -data object SocialRoute +data object SocialRoute : NavKey @Serializable -data object AnimeRoute +data object AnimeRoute : NavKey @Serializable -data object MangaRoute +data object MangaRoute : NavKey diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt new file mode 100644 index 000000000..bf6666b97 --- /dev/null +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt @@ -0,0 +1,24 @@ +package com.imashnake.animite.navigation.di + +import androidx.compose.animation.SharedTransitionScope +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.imashnake.animite.navigation.AnimeRoute +import com.imashnake.animite.navigation.Navigator +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped + +typealias EntryInstaller = EntryProviderScope.(SharedTransitionScope) -> Unit + +@Module +@InstallIn(ActivityRetainedComponent::class) +object NavigationModule { + + @Provides + @ActivityRetainedScoped + fun provideNavigator() = Navigator(AnimeRoute) + +} \ No newline at end of file diff --git a/profile/build.gradle.kts b/profile/build.gradle.kts index 909abd2ed..e04e7b398 100644 --- a/profile/build.gradle.kts +++ b/profile/build.gradle.kts @@ -12,7 +12,10 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } @@ -44,9 +47,6 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycleRuntimeKtx) - // Navigation - implementation(libs.androidx.navigationCommon) - // Compose implementation(libs.compose.animation) implementation(libs.compose.foundation) 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 6a1b6ee02..c69f31fb6 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt @@ -71,7 +71,6 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import coil3.compose.AsyncImage import com.boswelja.markdown.material3.MarkdownDocument import com.boswelja.markdown.material3.m3TextStyles @@ -108,14 +107,15 @@ fun ProfileScreen( onNavigateToSettings: (SettingsPage) -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, + viewModel: ProfileViewModel, contentWindowInsets: WindowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout), - viewModel: ProfileViewModel = hiltViewModel(), ) { val insetPaddingValues = contentWindowInsets.asPaddingValues() - val navigationComponentPaddingValues = when(LocalConfiguration.current.orientation) { + val navigationComponentPaddingValues = when (LocalConfiguration.current.orientation) { Configuration.ORIENTATION_PORTRAIT -> PaddingValues( bottom = dimensionResource(navigationR.dimen.navigation_bar_height) ) + else -> PaddingValues( start = dimensionResource(navigationR.dimen.navigation_rail_width) ) @@ -206,8 +206,10 @@ fun ProfileScreen( contentPadding = PaddingValues(top = LocalPaddings.current.large / 2) ) } + else -> ProgressIndicatorScreen(Modifier.padding(allPaddingValues)) } + else -> { SettingsIcon( onNavigateToSettings = onNavigateToSettings, @@ -531,6 +533,7 @@ private fun UserTabs( user = user, contentPadding = tabContentPadding, ) + ProfileTab.ANIME -> MediaTab( mediaCollection = animeCollection, onNavigateToMediaItem = onNavigateToMediaItem, @@ -538,6 +541,7 @@ private fun UserTabs( animatedVisibilityScope = animatedVisibilityScope, contentPadding = tabContentPadding, ) + ProfileTab.MANGA -> MediaTab( mediaCollection = mangaCollection, onNavigateToMediaItem = onNavigateToMediaItem, @@ -545,6 +549,7 @@ private fun UserTabs( animatedVisibilityScope = animatedVisibilityScope, contentPadding = tabContentPadding, ) + ProfileTab.FAVOURITES -> FavouritesTab( favouriteLists = user.favourites, onNavigateToMediaItem = onNavigateToMediaItem, diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt index aa930a6e6..587ceee99 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt @@ -1,15 +1,16 @@ package com.imashnake.animite.profile -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute import com.imashnake.animite.api.anilist.AnilistUserRepository import com.imashnake.animite.api.anilist.type.MediaType import com.imashnake.animite.api.preferences.PreferencesRepository import com.imashnake.animite.core.data.Resource import com.imashnake.animite.core.data.Resource.Companion.asResource import com.imashnake.animite.navigation.ProfileRoute +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -23,17 +24,15 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class ProfileViewModel @Inject constructor( +@HiltViewModel(assistedFactory = ProfileViewModel.Factory::class) +class ProfileViewModel @AssistedInject constructor( private val userRepository: AnilistUserRepository, private val preferencesRepository: PreferencesRepository, - savedStateHandle: SavedStateHandle + @Assisted val navArgs: ProfileRoute, ) : ViewModel() { - private val navArgs = savedStateHandle.toRoute() private val refreshTrigger = MutableSharedFlow() var useNetwork = false @@ -105,4 +104,9 @@ class ProfileViewModel @Inject constructor( } } } + + @AssistedFactory + interface Factory { + fun create(navArgs: ProfileRoute): ProfileViewModel + } } diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt new file mode 100644 index 000000000..5f0cd9977 --- /dev/null +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt @@ -0,0 +1,37 @@ +package com.imashnake.animite.profile.navigation.di + +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.ui.LocalNavAnimatedContentScope +import com.imashnake.animite.navigation.Navigator +import com.imashnake.animite.navigation.ProfileRoute +import com.imashnake.animite.navigation.di.EntryInstaller +import com.imashnake.animite.profile.ProfileScreen +import com.imashnake.animite.profile.ProfileViewModel +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +@Module +@InstallIn(ActivityRetainedComponent::class) +object NavigationModule { + + @Provides + @IntoSet + fun provideNavEntry( + navigator: Navigator, + ): EntryInstaller = { sharedScope -> + entry { args -> + ProfileScreen( + onNavigateToSettings = navigator::navigate, + onNavigateToMediaItem = navigator::navigate, + sharedTransitionScope = sharedScope, + animatedVisibilityScope = LocalNavAnimatedContentScope.current, + viewModel = hiltViewModel { factory -> + factory.create(args) + } + ) + } + } +} \ No newline at end of file diff --git a/settings/build.gradle.kts b/settings/build.gradle.kts index b3dc03d99..b54018248 100644 --- a/settings/build.gradle.kts +++ b/settings/build.gradle.kts @@ -12,7 +12,10 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } @@ -44,9 +47,6 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycleRuntimeKtx) - // Navigation - implementation(libs.androidx.navigationCommon) - // Compose implementation(libs.compose.animation) implementation(libs.compose.foundation) diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt index 08d333681..d4d0bc2eb 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt @@ -78,6 +78,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.NavKey import com.imashnake.animite.core.extensions.DayPart import com.imashnake.animite.core.extensions.horizontalOnly import com.imashnake.animite.core.extensions.plus @@ -111,11 +112,14 @@ fun SettingsPage( val scrollState = rememberScrollState() - val selectedTheme by viewModel.theme.filterNotNull().collectAsState(initial = Theme.DEVICE_THEME.name) - val useSystemColorScheme by viewModel.useSystemColorScheme.filterNotNull().collectAsState(initial = true) + val selectedTheme by viewModel.theme.filterNotNull() + .collectAsState(initial = Theme.DEVICE_THEME.name) + val useSystemColorScheme by viewModel.useSystemColorScheme.filterNotNull() + .collectAsState(initial = true) val haptic = LocalHapticFeedback.current - val isDevOptionsEnabled by viewModel.isDevOptionsEnabled.filterNotNull().collectAsState(initial = false) + val isDevOptionsEnabled by viewModel.isDevOptionsEnabled.filterNotNull() + .collectAsState(initial = false) val isDarkMode = selectedTheme == Theme.DARK.name || (selectedTheme == Theme.DEVICE_THEME.name && isSystemInDarkTheme()) @@ -293,45 +297,53 @@ fun SettingsPage( valueRange = 0f..24f, ) } + 1 -> { - Column(verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.small)) { - DayPart.entries.map { it.name }.plus(SYSTEM_DAY_PART).forEach { - Row( - horizontalArrangement = Arrangement.spacedBy(LocalPaddings.current.small), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(start = LocalPaddings.current.large) - .clip(CircleShape) - .clickable { - if (it != SYSTEM_DAY_PART) { - viewModel.setDayHour( - when(DayPart.valueOf(it)) { - DayPart.MORNING -> 6f - DayPart.AFTERNOON -> 12f - DayPart.EVENING -> 18f - DayPart.NIGHT -> 21f - } - ) - } else viewModel.setDayHour(null) - } - .padding( - top = LocalPaddings.current.tiny, - bottom = LocalPaddings.current.tiny, - start = LocalPaddings.current.tiny, - end = LocalPaddings.current.medium + Column( + verticalArrangement = Arrangement.spacedBy( + LocalPaddings.current.small + ) + ) { + DayPart.entries.map { it.name }.plus(SYSTEM_DAY_PART) + .forEach { + Row( + horizontalArrangement = Arrangement.spacedBy( + LocalPaddings.current.small + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(start = LocalPaddings.current.large) + .clip(CircleShape) + .clickable { + if (it != SYSTEM_DAY_PART) { + viewModel.setDayHour( + when (DayPart.valueOf(it)) { + DayPart.MORNING -> 6f + DayPart.AFTERNOON -> 12f + DayPart.EVENING -> 18f + DayPart.NIGHT -> 21f + } + ) + } else viewModel.setDayHour(null) + } + .padding( + top = LocalPaddings.current.tiny, + bottom = LocalPaddings.current.tiny, + start = LocalPaddings.current.tiny, + end = LocalPaddings.current.medium + ) + ) { + RadioButton( + selected = if (dayHour == null) it == SYSTEM_DAY_PART else it == dayHour?.toDayPart()?.name, + onClick = null, + ) + Text( + text = it, + style = MaterialTheme.typography.labelSmallEmphasized, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - ) { - RadioButton( - selected = if (dayHour == null) it == SYSTEM_DAY_PART else it == dayHour?.toDayPart()?.name, - onClick = null, - ) - Text( - text = it, - style = MaterialTheme.typography.labelSmallEmphasized, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + } } - } } } } @@ -389,6 +401,7 @@ private fun Items( ) { itemContent(index) } + Item.Orientation.VERTICAL -> VerticalItem( item = index to item, shape = RoundedCornerShape( @@ -549,7 +562,11 @@ private fun AboutItem( toast?.cancel() } if (isDevOptionsEnabled) { - toast = Toast.makeText(context, R.string.already_developer, Toast.LENGTH_SHORT) + toast = Toast.makeText( + context, + R.string.already_developer, + Toast.LENGTH_SHORT + ) toast?.show() return@combinedClickable } @@ -563,7 +580,8 @@ private fun AboutItem( } else if (devOptionsCount == 10) { enableDevOptions() onClick() - toast = Toast.makeText(context, R.string.now_developer, Toast.LENGTH_SHORT) + toast = + Toast.makeText(context, R.string.now_developer, Toast.LENGTH_SHORT) } toast?.show() }, @@ -573,7 +591,11 @@ private fun AboutItem( toast?.cancel() } disableDevOptions() - toast = Toast.makeText(context, R.string.disabled_dev_options, Toast.LENGTH_SHORT) + toast = Toast.makeText( + context, + R.string.disabled_dev_options, + Toast.LENGTH_SHORT + ) toast?.show() } } @@ -603,7 +625,9 @@ private fun AboutItem( Column( verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier.fillMaxHeight().weight(1f) + modifier = Modifier + .fillMaxHeight() + .weight(1f) ) { Text( text = ANIMITE, @@ -741,7 +765,9 @@ private fun PreviewItems() { ), onItemClick = {}, isDarkMode = false, - modifier = Modifier.fillMaxWidth().padding(horizontal = padding) + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = padding) ) { index -> when (index) { 0 -> Row( @@ -814,4 +840,4 @@ enum class Theme(@param:StringRes val theme: Int) { } @Serializable -data object SettingsPage +data object SettingsPage : NavKey diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt new file mode 100644 index 000000000..94fdaa2d1 --- /dev/null +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt @@ -0,0 +1,28 @@ +package com.imashnake.animite.settings.navigation.di + +import androidx.compose.animation.SharedTransitionScope +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.imashnake.animite.settings.SettingsPage +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +@Module +@InstallIn(ActivityRetainedComponent::class) +object NavigationModule { + + @Provides + @IntoSet + fun provideNavEntry( + versionName: String + ): EntryProviderScope.(SharedTransitionScope) -> Unit = { _ -> + entry { + SettingsPage( + versionName = versionName + ) + } + } +} \ No newline at end of file diff --git a/social/build.gradle.kts b/social/build.gradle.kts index 7284e0916..01d054f3f 100644 --- a/social/build.gradle.kts +++ b/social/build.gradle.kts @@ -4,13 +4,17 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.ksp) alias(libs.plugins.detekt) + alias(libs.plugins.hilt) } android { buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } @@ -25,6 +29,7 @@ kotlin { dependencies { implementation(projects.core) + implementation(projects.navigation) // AndroidX implementation(libs.androidx.activityCompose) @@ -46,6 +51,11 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.core) + // Hilt + implementation(libs.hilt.android) + implementation(libs.hilt.navigationCompose) + ksp(libs.hilt.android.compiler) + testImplementation(libs.test.junit) androidTestImplementation(libs.androidx.test.junit) diff --git a/social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt b/social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt new file mode 100644 index 000000000..853359097 --- /dev/null +++ b/social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt @@ -0,0 +1,26 @@ +package com.imashnake.animite.social.navigation.di + +import androidx.compose.animation.SharedTransitionScope +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.imashnake.animite.navigation.SocialRoute +import com.imashnake.animite.social.SocialScreen +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +@Module +@InstallIn(ActivityRetainedComponent::class) +object NavigationModule { + + @Provides + @IntoSet + fun provideNavEntry(): EntryProviderScope.(SharedTransitionScope) -> Unit = + { _ -> + entry { + SocialScreen() + } + } +} \ No newline at end of file From e4f9f5f3d9aee85f634fda712ec62090ffa29053 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Wed, 4 Mar 2026 00:11:39 +0000 Subject: [PATCH 02/10] Fix Navigation Bar/Rail with avatar & rotating icons --- .../animite/features/MainActivity.kt | 8 +++- .../animite/navigation/AnimatedAnimeIcon.kt | 41 +++++++++++++++++++ .../animite/navigation/AnimatedProfileIcon.kt | 34 +++++++++++++++ .../animite/navigation/NavigationBar.kt | 25 ++++++++--- .../animite/navigation/NavigationBarPaths.kt | 3 -- .../animite/navigation/NavigationRail.kt | 25 ++++++++--- .../animite/profile/AvatarViewModel.kt | 14 +++++++ 7 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt create mode 100644 navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedProfileIcon.kt create mode 100644 profile/src/main/kotlin/com/imashnake/animite/profile/AvatarViewModel.kt diff --git a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt index 49a8328f5..edeb337dd 100644 --- a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt +++ b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt @@ -47,6 +47,7 @@ import com.imashnake.animite.navigation.NavigationBarPaths import com.imashnake.animite.navigation.NavigationRail import com.imashnake.animite.navigation.Navigator import com.imashnake.animite.navigation.di.EntryInstaller +import com.imashnake.animite.profile.AvatarViewModel import com.imashnake.animite.profile.ProfileViewModel import com.imashnake.animite.settings.SettingsPage import com.imashnake.animite.settings.SettingsViewModel @@ -65,7 +66,8 @@ class MainActivity : ComponentActivity() { internal lateinit var navigator: Navigator private val settingsViewModel: SettingsViewModel by viewModels() - private val profileViewModel: ProfileViewModel by viewModels() + private val avatarViewModel: AvatarViewModel by viewModels() + var showSplashScreen = true override fun onCreate(savedInstanceState: Bundle?) { @@ -96,7 +98,7 @@ class MainActivity : ComponentActivity() { val dayHour by settingsViewModel.dayHour.collectAsState(initial = null) - val avatar by profileViewModel.viewerAvatar.collectAsState(initial = null) + val avatar by avatarViewModel.viewerAvatar.collectAsState(initial = null) AnimiteTheme( useDarkTheme = useDarkTheme, @@ -162,6 +164,7 @@ fun MainScreen( ) { NavigationRail( backStack = navigator.backStack, + avatar = avatar, onNavigate = navigator::navigate, ) } @@ -176,6 +179,7 @@ fun MainScreen( ) { NavigationBar( backStack = navigator.backStack, + avatar = avatar, onNavigate = navigator::navigate, ) } diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt new file mode 100644 index 000000000..73e71d092 --- /dev/null +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt @@ -0,0 +1,41 @@ +package com.imashnake.animite.navigation + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource + +@Composable +internal fun AnimatedAnimeIcon() { + val infiniteTransition = rememberInfiniteTransition() + val angle by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(12000, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + ) + Box { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.anime_inner), + contentDescription = stringResource(R.string.anime) + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.anime_outer), + contentDescription = stringResource(R.string.anime), + modifier = Modifier.graphicsLayer { rotationZ = angle } + ) + } +} \ No newline at end of file diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedProfileIcon.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedProfileIcon.kt new file mode 100644 index 000000000..aa89082af --- /dev/null +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedProfileIcon.kt @@ -0,0 +1,34 @@ +package com.imashnake.animite.navigation + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage + +@Composable +internal fun AnimatedProfileIcon( + avatar: String?, +) { + AnimatedContent(targetState = avatar) { + if (it != null) { + AsyncImage( + model = it, + contentDescription = "Avatar", + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background( + MaterialTheme.colorScheme.onSurface.copy( + 0.1f + ) + ) + ) + } + } +} diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt index 7484a8748..834e40ab6 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt @@ -30,6 +30,7 @@ import androidx.navigation3.runtime.NavKey fun NavigationBar( backStack: List, onNavigate: (NavKey) -> Unit, + avatar: String?, modifier: Modifier = Modifier, containerColor: Color = NavigationBarDefaults.containerColor, contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), @@ -53,14 +54,28 @@ fun NavigationBar( verticalAlignment = Alignment.CenterVertically, ) { NavigationBarPaths.entries.forEach { destination -> + val isSelected = backStack.contains(destination.route) + NavigationBarItem( - selected = backStack.contains(destination.route), + selected = isSelected, onClick = { onNavigate(destination.route) }, icon = { - Icon( - ImageVector.vectorResource(destination.icon), - contentDescription = stringResource(destination.iconDescription) - ) + when (destination) { + NavigationBarPaths.Profile if avatar != null -> { + AnimatedProfileIcon(avatar) + } + + NavigationBarPaths.Anime if isSelected -> { + AnimatedAnimeIcon() + } + + else -> { + Icon( + ImageVector.vectorResource(destination.icon), + contentDescription = stringResource(destination.iconDescription) + ) + } + } }, modifier = Modifier.height(dimensionResource(R.dimen.navigation_bar_height)), ) diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt index 0ab099c25..091b617c0 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBarPaths.kt @@ -2,9 +2,6 @@ package com.imashnake.animite.navigation import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation3.runtime.NavKey enum class NavigationBarPaths( diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt index 7ee1c963a..7737dd2ad 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt @@ -39,6 +39,7 @@ import androidx.navigation3.runtime.NavKey fun NavigationRail( backStack: List, onNavigate: (NavKey) -> Unit, + avatar: String?, modifier: Modifier = Modifier, containerColor: Color = NavigationBarDefaults.containerColor, contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), @@ -68,14 +69,28 @@ fun NavigationRail( horizontalAlignment = Alignment.CenterHorizontally, ) { NavigationBarPaths.entries.forEach { destination -> + val isSelected = backStack.contains(destination.route) + NavigationRailItem( - selected = backStack.contains(destination.route), + selected = isSelected, onClick = { onNavigate(destination.route) }, icon = { - Icon( - ImageVector.vectorResource(destination.icon), - contentDescription = stringResource(destination.iconDescription) - ) + when (destination) { + NavigationBarPaths.Profile if avatar != null -> { + AnimatedProfileIcon(avatar) + } + + NavigationBarPaths.Anime if isSelected -> { + AnimatedAnimeIcon() + } + + else -> { + Icon( + ImageVector.vectorResource(destination.icon), + contentDescription = stringResource(destination.iconDescription) + ) + } + } }, modifier = Modifier.width(dimensionResource(R.dimen.navigation_rail_width)) ) diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/AvatarViewModel.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/AvatarViewModel.kt new file mode 100644 index 000000000..2f8e4a090 --- /dev/null +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/AvatarViewModel.kt @@ -0,0 +1,14 @@ +package com.imashnake.animite.profile + +import androidx.lifecycle.ViewModel +import com.imashnake.animite.api.preferences.PreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class AvatarViewModel @Inject constructor( + preferencesRepository: PreferencesRepository +): ViewModel() { + + val viewerAvatar = preferencesRepository.viewerAvatar +} \ No newline at end of file From 23331ae59a944815f4fc4383f70a6135bc049869 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Wed, 4 Mar 2026 00:24:27 +0000 Subject: [PATCH 03/10] Fix some formatting --- .../com/imashnake/animite/media/MediaPage.kt | 7 +++++-- .../imashnake/animite/profile/ProfileScreen.kt | 6 ------ .../imashnake/animite/settings/SettingsPage.kt | 17 ++++++----------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt index efa1b5faf..6f2bea0ca 100644 --- a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt +++ b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt @@ -1033,14 +1033,17 @@ private fun MediaRankings( ) { Row( verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.spacedBy(LocalPaddings.current.tiny)) { + horizontalArrangement = Arrangement.spacedBy(LocalPaddings.current.tiny) + ) { Text(text = stringResource(timeSpan.res), Modifier.alignByBaseline()) when (timeSpan.index) { 1 -> year?.let { Text( text = it, fontSize = 10.sp, - modifier = Modifier.graphicsLayer { alpha = 0.5f }.alignByBaseline() + modifier = Modifier + .graphicsLayer { alpha = 0.5f } + .alignByBaseline() ) } 2 -> season?.let { 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 7515d6172..4306daadd 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt @@ -116,7 +116,6 @@ fun ProfileScreen( Configuration.ORIENTATION_PORTRAIT -> PaddingValues( bottom = dimensionResource(navigationR.dimen.navigation_bar_height) ) - else -> PaddingValues( start = dimensionResource(navigationR.dimen.navigation_rail_width) ) @@ -211,10 +210,8 @@ fun ProfileScreen( contentPadding = PaddingValues(top = LocalPaddings.current.large / 2) ) } - else -> ProgressIndicatorScreen(Modifier.padding(allPaddingValues)) } - else -> { SettingsIcon( onNavigateToSettings = onNavigateToSettings, @@ -538,7 +535,6 @@ private fun UserTabs( user = user, contentPadding = tabContentPadding, ) - ProfileTab.ANIME -> MediaTab( mediaCollection = animeCollection, onNavigateToMediaItem = onNavigateToMediaItem, @@ -546,7 +542,6 @@ private fun UserTabs( animatedVisibilityScope = animatedVisibilityScope, contentPadding = tabContentPadding, ) - ProfileTab.MANGA -> MediaTab( mediaCollection = mangaCollection, onNavigateToMediaItem = onNavigateToMediaItem, @@ -554,7 +549,6 @@ private fun UserTabs( animatedVisibilityScope = animatedVisibilityScope, contentPadding = tabContentPadding, ) - ProfileTab.FAVOURITES -> FavouritesTab( favouriteLists = user.favourites, onNavigateToMediaItem = onNavigateToMediaItem, diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt index a440d1de1..210694761 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt @@ -112,17 +112,14 @@ fun SettingsPage( val scrollState = rememberScrollState() - val selectedTheme by viewModel.theme.filterNotNull() - .collectAsState(initial = Theme.DEVICE_THEME.name) - val useSystemColorScheme by viewModel.useSystemColorScheme.filterNotNull() - .collectAsState(initial = true) + val selectedTheme by viewModel.theme.filterNotNull().collectAsState(initial = Theme.DEVICE_THEME.name) + val useSystemColorScheme by viewModel.useSystemColorScheme.filterNotNull().collectAsState(initial = true) val haptic = LocalHapticFeedback.current - val isDevOptionsEnabled by viewModel.isDevOptionsEnabled.filterNotNull() - .collectAsState(initial = false) + val isDevOptionsEnabled by viewModel.isDevOptionsEnabled.filterNotNull().collectAsState(initial = false) - val isDarkMode = selectedTheme == Theme.DARK.name || - (selectedTheme == Theme.DEVICE_THEME.name && isSystemInDarkTheme()) + val isDarkMode = + selectedTheme == Theme.DARK.name || (selectedTheme == Theme.DEVICE_THEME.name && isSystemInDarkTheme()) var devOptionsCount by remember { mutableIntStateOf(0) } @@ -300,9 +297,7 @@ fun SettingsPage( 1 -> { Column( - verticalArrangement = Arrangement.spacedBy( - LocalPaddings.current.small - ) + verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.small) ) { DayPart.entries.map { it.name }.plus(SYSTEM_DAY_PART) .forEach { From 742b5dbc8176dd44cbe5fbeb6443b42545bf8005 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Wed, 4 Mar 2026 00:27:23 +0000 Subject: [PATCH 04/10] Apply suggestions from code review --- app/build.gradle.kts | 5 +---- .../main/kotlin/com/imashnake/animite/ApplicationModule.kt | 2 +- .../animite/media/navigation/di/NavigationModule.kt | 2 +- navigation/build.gradle.kts | 5 +---- .../com/imashnake/animite/navigation/AnimatedAnimeIcon.kt | 2 +- .../com/imashnake/animite/navigation/di/NavigationModule.kt | 2 +- profile/build.gradle.kts | 5 +---- social/build.gradle.kts | 5 +---- 8 files changed, 8 insertions(+), 20 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 36f3c36b1..6a7420423 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,10 +32,7 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") signingConfig = signingConfigs.getByName( if (System.getenv("Animite") == "true") "release" else "debug" ) diff --git a/app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt b/app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt index 9a4df9c8f..cb7e4b2be 100644 --- a/app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt +++ b/app/src/main/kotlin/com/imashnake/animite/ApplicationModule.kt @@ -11,4 +11,4 @@ object ApplicationModule { @Provides fun provideVersionName() = BuildConfig.VERSION_NAME -} \ No newline at end of file +} diff --git a/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt b/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt index e360fda30..f065dc021 100644 --- a/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt +++ b/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt @@ -36,4 +36,4 @@ object NavigationModule { ) } } -} \ No newline at end of file +} diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts index 8860ec3e3..17223e7a0 100644 --- a/navigation/build.gradle.kts +++ b/navigation/build.gradle.kts @@ -11,10 +11,7 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt index 73e71d092..997ec43f2 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/AnimatedAnimeIcon.kt @@ -38,4 +38,4 @@ internal fun AnimatedAnimeIcon() { modifier = Modifier.graphicsLayer { rotationZ = angle } ) } -} \ No newline at end of file +} diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt index bf6666b97..57aba021f 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt @@ -21,4 +21,4 @@ object NavigationModule { @ActivityRetainedScoped fun provideNavigator() = Navigator(AnimeRoute) -} \ No newline at end of file +} diff --git a/profile/build.gradle.kts b/profile/build.gradle.kts index 4164597d3..bc6a9b61a 100644 --- a/profile/build.gradle.kts +++ b/profile/build.gradle.kts @@ -12,10 +12,7 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } diff --git a/social/build.gradle.kts b/social/build.gradle.kts index 01d054f3f..678b4a7e4 100644 --- a/social/build.gradle.kts +++ b/social/build.gradle.kts @@ -11,10 +11,7 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } From 5eba3790a53785c39ab4e77db0881fc3c95ed7eb Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Wed, 4 Mar 2026 00:30:13 +0000 Subject: [PATCH 05/10] Fix some formatting --- .../com/imashnake/animite/media/MediaPage.kt | 7 +++---- .../animite/settings/SettingsPage.kt | 20 +++++-------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt index 6f2bea0ca..cf2e261f3 100644 --- a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt +++ b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt @@ -512,9 +512,8 @@ fun MediaPage( .padding(paddingValues) .padding(bottom = LocalPaddings.current.large) .graphicsLayer { - val pageOffset = ( - creditPagerState.currentPage - page + creditPagerState.currentPageOffsetFraction - ).absoluteValue + val pageOffset = + (creditPagerState.currentPage - page + creditPagerState.currentPageOffsetFraction).absoluteValue alpha = lerp( start = 0f, @@ -1069,7 +1068,7 @@ private fun MediaRankings( slideIntoContainer(towards = Up, initialOffset = { it / 3 })) .togetherWith( fadeOut(animationSpec = tween(90)) + - slideOutOfContainer(towards = Down, targetOffset = { it / 3 }) + slideOutOfContainer(towards = Down, targetOffset = { it / 3 }) ) } ) { diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt index 210694761..925c87eb7 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt @@ -118,8 +118,8 @@ fun SettingsPage( val isDevOptionsEnabled by viewModel.isDevOptionsEnabled.filterNotNull().collectAsState(initial = false) - val isDarkMode = - selectedTheme == Theme.DARK.name || (selectedTheme == Theme.DEVICE_THEME.name && isSystemInDarkTheme()) + val isDarkMode = selectedTheme == Theme.DARK.name || + (selectedTheme == Theme.DEVICE_THEME.name && isSystemInDarkTheme()) var devOptionsCount by remember { mutableIntStateOf(0) } @@ -396,7 +396,6 @@ private fun Items( ) { itemContent(index) } - Item.Orientation.VERTICAL -> VerticalItem( item = index to item, shape = RoundedCornerShape( @@ -557,11 +556,7 @@ private fun AboutItem( toast?.cancel() } if (isDevOptionsEnabled) { - toast = Toast.makeText( - context, - R.string.already_developer, - Toast.LENGTH_SHORT - ) + toast = Toast.makeText(context, R.string.already_developer, Toast.LENGTH_SHORT) toast?.show() return@combinedClickable } @@ -575,8 +570,7 @@ private fun AboutItem( } else if (devOptionsCount == 10) { enableDevOptions() onClick() - toast = - Toast.makeText(context, R.string.now_developer, Toast.LENGTH_SHORT) + toast = Toast.makeText(context, R.string.now_developer, Toast.LENGTH_SHORT) } toast?.show() }, @@ -586,11 +580,7 @@ private fun AboutItem( toast?.cancel() } disableDevOptions() - toast = Toast.makeText( - context, - R.string.disabled_dev_options, - Toast.LENGTH_SHORT - ) + toast = Toast.makeText(context, R.string.disabled_dev_options, Toast.LENGTH_SHORT) toast?.show() } } From 06c8f80f131847dd5339e1e3ce10c9adc717fe51 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Wed, 4 Mar 2026 00:37:08 +0000 Subject: [PATCH 06/10] fix EOF --- .../imashnake/animite/anime/navigation/di/NavigationModule.kt | 2 +- .../imashnake/animite/manga/navigation/di/NavigationModule.kt | 2 +- .../main/kotlin/com/imashnake/animite/navigation/Navigator.kt | 2 +- .../imashnake/animite/profile/navigation/di/NavigationModule.kt | 2 +- .../animite/settings/navigation/di/NavigationModule.kt | 2 +- .../imashnake/animite/social/navigation/di/NavigationModule.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt b/anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt index cd956c7bd..c9772d069 100644 --- a/anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt +++ b/anime/src/main/kotlin/com/imashnake/animite/anime/navigation/di/NavigationModule.kt @@ -30,4 +30,4 @@ object NavigationModule { ) } } -} \ No newline at end of file +} diff --git a/manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt b/manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt index 3984fece7..011dd04ff 100644 --- a/manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt +++ b/manga/src/main/kotlin/com/imashnake/animite/manga/navigation/di/NavigationModule.kt @@ -30,4 +30,4 @@ object NavigationModule { ) } } -} \ No newline at end of file +} diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt index c0b9f11fe..3adb09532 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt @@ -24,4 +24,4 @@ class Navigator( fun popBack() { backStack.removeLastOrNull() } -} \ No newline at end of file +} diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt index 5f0cd9977..66fd9c1d6 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt @@ -34,4 +34,4 @@ object NavigationModule { ) } } -} \ No newline at end of file +} diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt index 94fdaa2d1..1dc2847a6 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/navigation/di/NavigationModule.kt @@ -25,4 +25,4 @@ object NavigationModule { ) } } -} \ No newline at end of file +} diff --git a/social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt b/social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt index 853359097..723b25186 100644 --- a/social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt +++ b/social/src/main/kotlin/com/imashnake/animite/social/navigation/di/NavigationModule.kt @@ -23,4 +23,4 @@ object NavigationModule { SocialScreen() } } -} \ No newline at end of file +} From fe1a1d6180dced757eb5d090dd3d857e56e20337 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Wed, 4 Mar 2026 00:38:58 +0000 Subject: [PATCH 07/10] Apply suggestions from code review --- settings/build.gradle.kts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/settings/build.gradle.kts b/settings/build.gradle.kts index 355f06f1a..28e052a53 100644 --- a/settings/build.gradle.kts +++ b/settings/build.gradle.kts @@ -12,10 +12,7 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } From e17ebfcdaba27b6e7631d3636bb427bf0e699e22 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Fri, 6 Mar 2026 22:26:47 +0000 Subject: [PATCH 08/10] delete partially used EntryInstaller --- .../com/imashnake/animite/features/MainActivity.kt | 9 +++++---- .../imashnake/animite/navigation/di/NavigationModule.kt | 5 ----- .../animite/profile/navigation/di/NavigationModule.kt | 6 ++++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt index edeb337dd..3fb6adfc5 100644 --- a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt +++ b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt @@ -8,6 +8,7 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally @@ -33,6 +34,8 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay @@ -46,9 +49,7 @@ import com.imashnake.animite.navigation.NavigationBar import com.imashnake.animite.navigation.NavigationBarPaths import com.imashnake.animite.navigation.NavigationRail import com.imashnake.animite.navigation.Navigator -import com.imashnake.animite.navigation.di.EntryInstaller import com.imashnake.animite.profile.AvatarViewModel -import com.imashnake.animite.profile.ProfileViewModel import com.imashnake.animite.settings.SettingsPage import com.imashnake.animite.settings.SettingsViewModel import com.imashnake.animite.settings.Theme @@ -60,7 +61,7 @@ import javax.inject.Inject class MainActivity : ComponentActivity() { @Inject - internal lateinit var entryProviders: Set<@JvmSuppressWildcards EntryInstaller> + internal lateinit var entryProviders: Set<@JvmSuppressWildcards EntryProviderScope.(SharedTransitionScope) -> Unit> @Inject internal lateinit var navigator: Navigator @@ -122,7 +123,7 @@ class MainActivity : ComponentActivity() { @Composable fun MainScreen( navigator: Navigator, - navEntries: Set, + navEntries: Set.(SharedTransitionScope) -> Unit>, avatar: String?, modifier: Modifier = Modifier, ) { diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt index 57aba021f..5afd642a6 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/di/NavigationModule.kt @@ -1,8 +1,5 @@ package com.imashnake.animite.navigation.di -import androidx.compose.animation.SharedTransitionScope -import androidx.navigation3.runtime.EntryProviderScope -import androidx.navigation3.runtime.NavKey import com.imashnake.animite.navigation.AnimeRoute import com.imashnake.animite.navigation.Navigator import dagger.Module @@ -11,8 +8,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.android.scopes.ActivityRetainedScoped -typealias EntryInstaller = EntryProviderScope.(SharedTransitionScope) -> Unit - @Module @InstallIn(ActivityRetainedComponent::class) object NavigationModule { diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt index 66fd9c1d6..89d97f1d9 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/navigation/di/NavigationModule.kt @@ -1,10 +1,12 @@ package com.imashnake.animite.profile.navigation.di +import androidx.compose.animation.SharedTransitionScope import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.imashnake.animite.navigation.Navigator import com.imashnake.animite.navigation.ProfileRoute -import com.imashnake.animite.navigation.di.EntryInstaller import com.imashnake.animite.profile.ProfileScreen import com.imashnake.animite.profile.ProfileViewModel import dagger.Module @@ -21,7 +23,7 @@ object NavigationModule { @IntoSet fun provideNavEntry( navigator: Navigator, - ): EntryInstaller = { sharedScope -> + ): EntryProviderScope.(SharedTransitionScope) -> Unit = { sharedScope -> entry { args -> ProfileScreen( onNavigateToSettings = navigator::navigate, From 224e4ffc825caf46380eb614ffde0e86f5c95682 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Sat, 7 Mar 2026 00:38:20 +0000 Subject: [PATCH 09/10] fix back handler going back to anime route --- .../animite/features/MainActivity.kt | 8 +++++-- .../com/imashnake/animite/media/MediaPage.kt | 5 +++-- .../media/navigation/di/NavigationModule.kt | 1 - .../animite/navigation/NavigationBar.kt | 2 +- .../animite/navigation/NavigationRail.kt | 2 +- .../imashnake/animite/navigation/Navigator.kt | 21 +++++++++++++++---- 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt index 3fb6adfc5..19c602641 100644 --- a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt +++ b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt @@ -127,7 +127,7 @@ fun MainScreen( avatar: String?, modifier: Modifier = Modifier, ) { - val isNavBarVisible by remember(navigator.backStack) { + val isNavBarVisible by remember(navigator.backStack.size) { derivedStateOf { NavigationBarPaths.entries.any { it.route == navigator.backStack.lastOrNull() } } @@ -140,12 +140,16 @@ fun MainScreen( LocalContentColor provides MaterialTheme.colorScheme.onBackground ) { SharedTransitionLayout { + val backstack = navigator.backStack + if (backstack.isEmpty()) return@SharedTransitionLayout + NavDisplay( + onBack = navigator::onBackPressed, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator() ), - backStack = navigator.backStack, + backStack = backstack, entryProvider = entryProvider { navEntries.forEach { builder -> builder(this@SharedTransitionLayout) diff --git a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt index 4b4fcd3e8..27a327a53 100644 --- a/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt +++ b/media/src/main/kotlin/com/imashnake/animite/media/MediaPage.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.os.Build import android.view.RoundedCorner import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Down import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Up @@ -174,7 +175,6 @@ private const val RELATIONS = "Relations" "LongMethod" ) fun MediaPage( - onBack: () -> Unit, onNavigateToMediaItem: (MediaPage) -> Unit, useDarkTheme: Boolean, sharedTransitionScope: SharedTransitionScope, @@ -185,6 +185,7 @@ fun MediaPage( ) { val insetPaddingValues = contentWindowInsets.asPaddingValues() val horizontalInsets = insetPaddingValues.horizontalOnly + val dispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher val scrollState = rememberScrollState() @@ -451,7 +452,7 @@ fun MediaPage( top = LocalPaddings.current.small ) .clip(CircleShape) - .clickable(enabled = !isExpanded) { onBack() } + .clickable(enabled = !isExpanded) { dispatcher?.onBackPressed() } .padding(LocalPaddings.current.small), tint = if (isSystemInDarkTheme()) Color.White else Color.Black ) diff --git a/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt b/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt index f065dc021..cf2298d75 100644 --- a/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt +++ b/media/src/main/kotlin/com/imashnake/animite/media/navigation/di/NavigationModule.kt @@ -25,7 +25,6 @@ object NavigationModule { ): EntryProviderScope.(SharedTransitionScope) -> Unit = { sharedScope -> entry { args -> MediaPage( - onBack = navigator::popBack, onNavigateToMediaItem = navigator::navigate, useDarkTheme = true, // fix? sharedTransitionScope = sharedScope, diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt index 834e40ab6..02f9abdb8 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationBar.kt @@ -54,7 +54,7 @@ fun NavigationBar( verticalAlignment = Alignment.CenterVertically, ) { NavigationBarPaths.entries.forEach { destination -> - val isSelected = backStack.contains(destination.route) + val isSelected = backStack.lastOrNull() == destination.route NavigationBarItem( selected = isSelected, diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt index 7737dd2ad..015e38883 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/NavigationRail.kt @@ -69,7 +69,7 @@ fun NavigationRail( horizontalAlignment = Alignment.CenterHorizontally, ) { NavigationBarPaths.entries.forEach { destination -> - val isSelected = backStack.contains(destination.route) + val isSelected = backStack.lastOrNull() == destination.route NavigationRailItem( selected = isSelected, diff --git a/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt b/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt index 3adb09532..f5cc4b67c 100644 --- a/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt +++ b/navigation/src/main/kotlin/com/imashnake/animite/navigation/Navigator.kt @@ -12,16 +12,29 @@ class Navigator( val backStack = NavBackStack(startDestination) fun navigate(navKey: NavKey, allowMultiple: Boolean = false) { - if (!allowMultiple && backStack.contains(navKey)) return + when { + navKey is AnimeRoute -> { + if (backStack.lastOrNull() is AnimeRoute) return popBackTo(AnimeRoute) + } + !allowMultiple && backStack.contains(navKey) -> return + } if (NavigationBarPaths.entries.any { it.route == navKey }) { - backStack.clear() + backStack.removeAll { it != AnimeRoute } } backStack.add(navKey) } - fun popBack() { - backStack.removeLastOrNull() + fun onBackPressed() { + if (backStack.lastOrNull() is AnimeRoute) { + backStack.clear() + } else { + backStack.removeLastOrNull() + } + } + + fun popBackTo(navKey: NavKey) { + backStack.removeAll { it != navKey } } } From 539233fdf8b07b065f5280854cc7a39fe9835ee8 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Sat, 7 Mar 2026 00:51:11 +0000 Subject: [PATCH 10/10] allow navdisplay to share transition scope --- .../kotlin/com/imashnake/animite/features/MainActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt index 19c602641..201c7bceb 100644 --- a/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt +++ b/app/src/main/kotlin/com/imashnake/animite/features/MainActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.viewModels import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.SizeTransform import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally @@ -154,7 +155,8 @@ fun MainScreen( navEntries.forEach { builder -> builder(this@SharedTransitionLayout) } - } + }, + sharedTransitionScope = this, ) } }