From 885ed5a018542c0031488b54be0fd073e5db5bf8 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 30 Mar 2025 04:32:29 +0300 Subject: [PATCH] voyager -> androidx.compose.navigation --- composeApp/build.gradle.kts | 11 +- .../overseerr/model/Platform.android.kt | 2 +- .../meloda/overseerr/theme/Theme.android.kt | 5 + .../kotlin/dev/meloda/overseerr/App.kt | 34 ++- .../dev/meloda/overseerr/model/Platform.kt | 2 +- .../screens/login/presentation/LoginScreen.kt | 202 +++++++------- .../overseerr/screens/main/MainScreen.kt | 119 ++++---- .../requests/presentation/RequestsScreen.kt | 256 +++++++++--------- .../screens/url/presentation/UrlScreen.kt | 157 ++++++----- .../dev/meloda/overseerr/theme/Theme.kt | 4 + .../meloda/overseerr/model/Platform.ios.kt | 2 +- .../dev/meloda/overseerr/theme/Theme.ios.kt | 5 + .../meloda/overseerr/model/Platform.jvm.kt | 2 +- .../dev/meloda/overseerr/theme/Theme.jvm.kt | 5 + .../meloda/overseerr/model/Platform.wasmJs.kt | 2 +- .../meloda/overseerr/theme/Theme.wasmJs.kt | 13 + gradle/libs.versions.toml | 5 +- 17 files changed, 442 insertions(+), 384 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 1a39407..aca1fb4 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -60,8 +60,7 @@ kotlin { implementation(compose.material3) implementation(compose.materialIconsExtended) implementation(compose.components.resources) - implementation(libs.voyager.navigator) - implementation(libs.voyager.transitions) + implementation(libs.androidx.navigation.compose) implementation(libs.coil) implementation(libs.coil.network.ktor) implementation(libs.kotlinx.coroutines.core) @@ -165,7 +164,13 @@ android { getByName("release") { signingConfig = signingConfigs.getByName("release") - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true + + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } compileOptions { diff --git a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/model/Platform.android.kt b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/model/Platform.android.kt index 14fae78..b35f2b1 100644 --- a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/model/Platform.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/model/Platform.android.kt @@ -1,6 +1,6 @@ package dev.meloda.overseerr.model -actual class Platform actual constructor() { +internal actual class Platform actual constructor() { actual val name: String get() = "Android" } diff --git a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/theme/Theme.android.kt b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/theme/Theme.android.kt index 9fd46ba..da52486 100644 --- a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/theme/Theme.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/theme/Theme.android.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowInsetsControllerCompat +import androidx.navigation.NavController @Composable internal actual fun SystemAppearance(isDark: Boolean) { @@ -16,4 +17,8 @@ internal actual fun SystemAppearance(isDark: Boolean) { isAppearanceLightNavigationBars = isDark } } +} + +@Composable +internal actual fun NavigationSettings(navController: NavController) { } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt index a2bf224..7c350ae 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt @@ -7,11 +7,16 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.transitions.FadeTransition +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import dev.meloda.overseerr.screens.login.presentation.LoginScreen import dev.meloda.overseerr.screens.main.MainScreen +import dev.meloda.overseerr.screens.requests.presentation.RequestsScreen +import dev.meloda.overseerr.screens.url.presentation.UrlScreen import dev.meloda.overseerr.settings.SettingsController import dev.meloda.overseerr.theme.AppTheme +import dev.meloda.overseerr.theme.NavigationSettings import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import org.koin.compose.KoinContext @@ -32,10 +37,31 @@ internal fun App() = KoinContext { settingsController.loadAppSettings() } + val navController = rememberNavController() + + NavigationSettings(navController) + AppTheme(themeMode = settings.themeMode) { Surface(modifier = Modifier.fillMaxSize()) { - Navigator(MainScreen()) { navigator -> - FadeTransition(navigator) + NavHost( + navController = navController, + startDestination = MainScreen + ) { + composable { + MainScreen(navController) + } + + composable { + LoginScreen(onBack = navController::popBackStack) + } + + composable { + RequestsScreen(onBack = navController::popBackStack) + } + + composable { + UrlScreen(onBack = navController::popBackStack) + } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt index 598bcb5..28919d0 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt @@ -1,5 +1,5 @@ package dev.meloda.overseerr.model -expect class Platform() { +internal expect class Platform() { val name: String } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt index 5c981f6..f4743e1 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/presentation/LoginScreen.kt @@ -8,8 +8,11 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction @@ -18,117 +21,112 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow import dev.meloda.overseerr.screens.login.LoginViewModel import dev.meloda.overseerr.screens.login.LoginViewModelImpl import dev.meloda.overseerr.screens.login.model.LoginScreenState +import kotlinx.serialization.Serializable import org.koin.compose.viewmodel.koinViewModel -class LoginScreen : Screen { +@Serializable +data object LoginScreen - @OptIn(ExperimentalMaterial3Api::class) - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val viewModel: LoginViewModel = koinViewModel() - val screenState: LoginScreenState by viewModel.screenState.collectAsStateWithLifecycle() +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginScreen(onBack: () -> Unit = {}) { + val viewModel: LoginViewModel = koinViewModel() + val screenState: LoginScreenState by viewModel.screenState.collectAsStateWithLifecycle() - var loginValue by rememberSaveable { - mutableStateOf(screenState.login) - } - var passwordValue by rememberSaveable { - mutableStateOf(screenState.password) - } + var loginValue by rememberSaveable { + mutableStateOf(screenState.login) + } + var passwordValue by rememberSaveable { + mutableStateOf(screenState.password) + } - Scaffold( - topBar = { - TopAppBar( - title = { - Text(text = "Log in") - }, - navigationIcon = { - IconButton( - onClick = navigator::pop - ) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = null - ) - } + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = "Log in") + }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null + ) } - ) - } - ) { padding -> - Column( - modifier = Modifier.fillMaxSize() - .padding(padding), - horizontalAlignment = Alignment.CenterHorizontally - ) { - TextField( - modifier = Modifier.fillMaxWidth(0.9f), - value = loginValue, - onValueChange = { newText -> - loginValue = newText - viewModel.onLoginInputChanged(newText) - }, - placeholder = { Text(text = "Login") }, - isError = screenState.isLoginEmptyError, - singleLine = true - ) - - Spacer(modifier = Modifier.height(16.dp)) - - TextField( - modifier = Modifier.fillMaxWidth(0.9f), - value = passwordValue, - onValueChange = { newText -> - passwordValue = newText - viewModel.onPasswordInputChanged(newText) - }, - placeholder = { Text(text = "Password") }, - isError = screenState.isPasswordEmptyError, - trailingIcon = { - IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) { - Icon( - imageVector = if (screenState.isPasswordVisible) { - Icons.Rounded.VisibilityOff - } else { - Icons.Rounded.Visibility - }, - contentDescription = if (screenState.isPasswordVisible) "Password visible icon" - else "Password invisible icon" - ) - } - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Go, - keyboardType = KeyboardType.Password - ), - visualTransformation = if (screenState.isPasswordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - singleLine = true - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { - - } - ) { - Text(text = "Authorize") - Spacer(modifier = Modifier.width(4.dp)) - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowForward, - contentDescription = null - ) } + ) + } + ) { padding -> + Column( + modifier = Modifier.fillMaxSize() + .padding(padding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier.fillMaxWidth(0.9f), + value = loginValue, + onValueChange = { newText -> + loginValue = newText + viewModel.onLoginInputChanged(newText) + }, + placeholder = { Text(text = "Login") }, + isError = screenState.isLoginEmptyError, + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TextField( + modifier = Modifier.fillMaxWidth(0.9f), + value = passwordValue, + onValueChange = { newText -> + passwordValue = newText + viewModel.onPasswordInputChanged(newText) + }, + placeholder = { Text(text = "Password") }, + isError = screenState.isPasswordEmptyError, + trailingIcon = { + IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) { + Icon( + imageVector = if (screenState.isPasswordVisible) { + Icons.Rounded.VisibilityOff + } else { + Icons.Rounded.Visibility + }, + contentDescription = if (screenState.isPasswordVisible) "Password visible icon" + else "Password invisible icon" + ) + } + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Go, + keyboardType = KeyboardType.Password + ), + visualTransformation = if (screenState.isPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { + + } + ) { + Text(text = "Authorize") + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowForward, + contentDescription = null + ) } } } -} +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt index d650a1e..54208c0 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt @@ -10,85 +10,82 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow +import androidx.navigation.NavController import dev.meloda.overseerr.screens.login.presentation.LoginScreen import dev.meloda.overseerr.screens.requests.presentation.RequestsScreen import dev.meloda.overseerr.screens.url.presentation.UrlScreen import dev.meloda.overseerr.settings.SettingsController import dev.meloda.overseerr.settings.model.ThemeMode import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import org.koin.compose.koinInject -class MainScreen : Screen { +@Serializable +data object MainScreen - @OptIn(ExperimentalMaterial3Api::class) - @Composable - override fun Content() { - val coroutineScope = rememberCoroutineScope() +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen(navController: NavController) { + val coroutineScope = rememberCoroutineScope() - val navigator = LocalNavigator.currentOrThrow + val settingsController: SettingsController = koinInject() + val settings by settingsController.settings.collectAsStateWithLifecycle() - val settingsController: SettingsController = koinInject() - val settings by settingsController.settings.collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "Main screen") }, + actions = { + TextButton( + onClick = { + val newThemeMode = ThemeMode.entries.getOrElse( + ThemeMode.entries.indexOf(settings.themeMode) + 1 + ) { ThemeMode.System } - Scaffold( - topBar = { - TopAppBar( - title = { Text(text = "Main screen") }, - actions = { - TextButton( - onClick = { - val newThemeMode = ThemeMode.entries.getOrElse( - ThemeMode.entries.indexOf(settings.themeMode) + 1 - ) { ThemeMode.System } - - settingsController.updateThemeMode(newThemeMode) - coroutineScope.launch { - settingsController.saveAppSettings() - } + settingsController.updateThemeMode(newThemeMode) + coroutineScope.launch { + settingsController.saveAppSettings() } - ) { - Text( - text = when (settings.themeMode) { - ThemeMode.System -> "System" - ThemeMode.Dark -> "Dark" - ThemeMode.Light -> "Light" - } - ) } + ) { + Text( + text = when (settings.themeMode) { + ThemeMode.System -> "System" + ThemeMode.Dark -> "Dark" + ThemeMode.Light -> "Light" + } + ) } - ) - } - ) { padding -> - Row( - modifier = Modifier - .padding(padding) - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), + } + ) + } + ) { padding -> + Row( + modifier = Modifier + .padding(padding) + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Button( + onClick = { navController.navigate(UrlScreen) }, + modifier = Modifier.weight(0.3f) ) { - Button( - onClick = { navigator.push(UrlScreen()) }, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Url") - } + Text(text = "Url") + } - Button( - onClick = { navigator.push(LoginScreen()) }, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Login") - } + Button( + onClick = { navController.navigate(LoginScreen) }, + modifier = Modifier.weight(0.3f) + ) { + Text(text = "Login") + } - Button( - onClick = { navigator.push(RequestsScreen()) }, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Requests") - } + Button( + onClick = { navController.navigate(RequestsScreen) }, + modifier = Modifier.weight(0.3f) + ) { + Text(text = "Requests") } } } -} +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt index d79e2d1..c671787 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt @@ -12,161 +12,165 @@ import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import dev.chrisbanes.haze.* +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.overseerr.screens.requests.RequestsViewModel import dev.meloda.overseerr.screens.requests.RequestsViewModelImpl import dev.meloda.overseerr.screens.requests.model.RequestsScreenState import kotlinx.coroutines.delay +import kotlinx.serialization.Serializable import org.koin.compose.viewmodel.koinViewModel import kotlin.time.Duration.Companion.seconds -class RequestsScreen : Screen { +@Serializable +data object RequestsScreen - @OptIn( - ExperimentalMaterial3Api::class, - ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3Api::class - ) - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val viewModel: RequestsViewModel = koinViewModel() - val screenState: RequestsScreenState by viewModel.screenState.collectAsStateWithLifecycle() +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class +) +@Composable +fun RequestsScreen( + onBack: () -> Unit = {} +) { + val viewModel: RequestsViewModel = koinViewModel() + val screenState: RequestsScreenState by viewModel.screenState.collectAsStateWithLifecycle() - val hazeState = remember { HazeState() } - val hazeStyle = HazeMaterials.ultraThin() + val hazeState = remember { HazeState() } + val hazeStyle = HazeMaterials.ultraThin() - val refreshState = rememberPullToRefreshState() + val refreshState = rememberPullToRefreshState() - LaunchedEffect(screenState) { - if (screenState.apiInfo != null) { - delay(5.seconds) - viewModel.onSuccessMessageShown() - } - - if (screenState.apiErrorText != null) { - delay(5.seconds) - viewModel.onErrorMessageShown() - } + LaunchedEffect(screenState) { + if (screenState.apiInfo != null) { + delay(5.seconds) + viewModel.onSuccessMessageShown() } - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - title = { Text(text = "Requests") }, - navigationIcon = { - IconButton(onClick = navigator::pop) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = null + if (screenState.apiErrorText != null) { + delay(5.seconds) + viewModel.onErrorMessageShown() + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { Text(text = "Requests") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + modifier = Modifier + .hazeEffect( + state = hazeState, + style = hazeStyle + ).fillMaxWidth(), + actions = { + IconButton( + onClick = viewModel::onRefresh + ) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + } + } + ) + } + ) { padding -> + val bottomPadding = padding.calculateBottomPadding() + + Box( + modifier = Modifier + .fillMaxSize() + .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) + .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .hazeSource(state = hazeState) + .pullToRefresh( + isRefreshing = screenState.isLoading, + state = refreshState, + onRefresh = viewModel::onRefresh + ) + ) { + item { + Spacer(modifier = Modifier.height(padding.calculateTopPadding())) + } + item { + AnimatedVisibility(screenState.apiErrorText != null || screenState.apiInfo != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + if (screenState.apiInfo != null) Color(0xffb00b69) + else Color.Red + ), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = screenState.apiErrorText ?: screenState.apiInfo.toString(), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(10.dp) ) } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + } + } + items(items = screenState.dummyItems) { index -> + Text( + text = "Text #${index + 1}", + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier.background(Color.Red) + ) + Spacer(modifier = Modifier.height(64.dp)) + } + item { + Spacer(modifier = Modifier.height(bottomPadding)) + } + } + + Indicator( + state = refreshState, + isRefreshing = screenState.isLoading, + modifier = Modifier.align(Alignment.TopCenter) + .padding(top = padding.calculateTopPadding()) + ) + + if (bottomPadding.value > 0) { + Box( modifier = Modifier + .align(Alignment.BottomCenter) .hazeEffect( state = hazeState, style = hazeStyle - ).fillMaxWidth(), - actions = { - IconButton( - onClick = viewModel::onRefresh - ) { - Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = null - ) - } - } - ) - } - ) { padding -> - val bottomPadding = padding.calculateBottomPadding() - - Box( - modifier = Modifier - .fillMaxSize() - .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) - .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .hazeSource(state = hazeState) - .pullToRefresh( - isRefreshing = screenState.isLoading, - state = refreshState, - onRefresh = viewModel::onRefresh ) - ) { - item { - Spacer(modifier = Modifier.height(padding.calculateTopPadding())) - } - item { - AnimatedVisibility(screenState.apiErrorText != null || screenState.apiInfo != null) { - Box( - modifier = Modifier - .fillMaxWidth() - .background( - if (screenState.apiInfo != null) Color(0xffb00b69) - else Color.Red - ), - contentAlignment = Alignment.CenterStart - ) { - Text( - text = screenState.apiErrorText ?: screenState.apiInfo.toString(), - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier.padding(10.dp) - ) - } - } - } - items(items = screenState.dummyItems) { index -> - Text( - text = "Text #${index + 1}", - style = MaterialTheme.typography.headlineLarge, - modifier = Modifier.background(Color.Red) - ) - Spacer(modifier = Modifier.height(64.dp)) - } - item { - Spacer(modifier = Modifier.height(bottomPadding)) - } - } - - Indicator( - state = refreshState, - isRefreshing = screenState.isLoading, - modifier = Modifier.align(Alignment.TopCenter) - .padding(top = padding.calculateTopPadding()) + .background(Color.Transparent) + .height(bottomPadding) + .fillMaxWidth() ) - - if (bottomPadding.value > 0) { - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .hazeEffect( - state = hazeState, - style = hazeStyle - ) - .background(Color.Transparent) - .height(bottomPadding) - .fillMaxWidth() - ) - } } } } -} +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt index 3759d8e..b64f540 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt @@ -13,99 +13,96 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow import dev.meloda.overseerr.screens.url.UrlViewModel import dev.meloda.overseerr.screens.url.UrlViewModelImpl +import kotlinx.serialization.Serializable import org.koin.compose.viewmodel.koinViewModel -class UrlScreen : Screen { +@Serializable +data object UrlScreen - @OptIn(ExperimentalMaterial3Api::class) - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val viewModel: UrlViewModel = koinViewModel() - val screenState by viewModel.screenState.collectAsStateWithLifecycle() +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UrlScreen(onBack: () -> Unit = {}) { + val viewModel: UrlViewModel = koinViewModel() + val screenState by viewModel.screenState.collectAsStateWithLifecycle() - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - title = { Text(text = "Url") }, - navigationIcon = { - IconButton(onClick = navigator::pop) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = null - ) - } + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { Text(text = "Url") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null + ) } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier.fillMaxWidth(), + value = screenState.url, + onValueChange = viewModel::onUrlInputChanged, + placeholder = { Text(text = "Url") }, + isError = screenState.isWrongUrlError, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Go, + keyboardType = KeyboardType.Uri ) - } - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = screenState.plexToken, + onValueChange = viewModel::onPlexTokenInputChanged, + placeholder = { Text(text = "Token") }, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Go + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - TextField( - modifier = Modifier.fillMaxWidth(), - value = screenState.url, - onValueChange = viewModel::onUrlInputChanged, - placeholder = { Text(text = "Url") }, - isError = screenState.isWrongUrlError, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Go, - keyboardType = KeyboardType.Uri - ) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = screenState.plexToken, - onValueChange = viewModel::onPlexTokenInputChanged, - placeholder = { Text(text = "Token") }, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Go - ) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp) + Button( + onClick = viewModel::onLoadButtonClicked, + modifier = Modifier.weight(0.3f) ) { - Button( - onClick = viewModel::onLoadButtonClicked, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Load") - } + Text(text = "Load") + } - Button( - onClick = viewModel::onSaveButtonClicked, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Save") - } + Button( + onClick = viewModel::onSaveButtonClicked, + modifier = Modifier.weight(0.3f) + ) { + Text(text = "Save") + } - Button( - onClick = viewModel::onTestButtonClicked, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Test") - } + Button( + onClick = viewModel::onTestButtonClicked, + modifier = Modifier.weight(0.3f) + ) { + Text(text = "Test") } } } } -} +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt index d449c22..05f9164 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.* +import androidx.navigation.NavController import dev.meloda.overseerr.settings.model.ThemeMode internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } @@ -36,3 +37,6 @@ internal fun AppTheme( @Composable internal expect fun SystemAppearance(isDark: Boolean) + +@Composable +internal expect fun NavigationSettings(navController: NavController) diff --git a/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/model/Platform.ios.kt b/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/model/Platform.ios.kt index e3b14e2..9da7463 100644 --- a/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/model/Platform.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/model/Platform.ios.kt @@ -1,6 +1,6 @@ package dev.meloda.overseerr.model -actual class Platform actual constructor() { +internal actual class Platform actual constructor() { actual val name: String get() = "iOS" } diff --git a/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/theme/Theme.ios.kt b/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/theme/Theme.ios.kt index 3a02dcc..4d8f8f9 100644 --- a/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/theme/Theme.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/theme/Theme.ios.kt @@ -2,6 +2,7 @@ package dev.meloda.overseerr.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.navigation.NavController import platform.UIKit.UIApplication import platform.UIKit.UIStatusBarStyleDarkContent import platform.UIKit.UIStatusBarStyleLightContent @@ -14,4 +15,8 @@ internal actual fun SystemAppearance(isDark: Boolean) { if (isDark) UIStatusBarStyleDarkContent else UIStatusBarStyleLightContent ) } +} + +@Composable +internal actual fun NavigationSettings(navController: NavController) { } \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/model/Platform.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/model/Platform.jvm.kt index e3520fd..f4501ab 100644 --- a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/model/Platform.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/model/Platform.jvm.kt @@ -1,6 +1,6 @@ package dev.meloda.overseerr.model -actual class Platform actual constructor() { +internal actual class Platform actual constructor() { actual val name: String get() = "JVM" } diff --git a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/theme/Theme.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/theme/Theme.jvm.kt index 7b34bdd..38c501b 100644 --- a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/theme/Theme.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/theme/Theme.jvm.kt @@ -1,7 +1,12 @@ package dev.meloda.overseerr.theme import androidx.compose.runtime.Composable +import androidx.navigation.NavController @Composable internal actual fun SystemAppearance(isDark: Boolean) { +} + +@Composable +internal actual fun NavigationSettings(navController: NavController) { } \ No newline at end of file diff --git a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/model/Platform.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/model/Platform.wasmJs.kt index d3c0331..243d831 100644 --- a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/model/Platform.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/model/Platform.wasmJs.kt @@ -1,5 +1,5 @@ package dev.meloda.overseerr.model -actual class Platform actual constructor() { +internal actual class Platform actual constructor() { actual val name: String = "JS" } diff --git a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/theme/Theme.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/theme/Theme.wasmJs.kt index d8e4d65..e076ef5 100644 --- a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/theme/Theme.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/theme/Theme.wasmJs.kt @@ -1,7 +1,20 @@ package dev.meloda.overseerr.theme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.navigation.ExperimentalBrowserHistoryApi +import androidx.navigation.NavController +import androidx.navigation.bindToNavigation +import kotlinx.browser.window @Composable internal actual fun SystemAppearance(isDark: Boolean) { } + +@OptIn(ExperimentalBrowserHistoryApi::class) +@Composable +internal actual fun NavigationSettings(navController: NavController) { + LaunchedEffect(navController) { + window.bindToNavigation(navController) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ac05cc..5dae97a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,6 @@ agp = "8.7.3" androidx-lifecycle = "2.8.4" androidx-activity-compose = "1.10.1" androidx-uitest = "1.7.8" -voyager = "1.1.0-beta03" coil = "3.0.4" kotlinx-coroutines = "1.10.1" ktor = "3.0.1" @@ -17,6 +16,7 @@ haze = "1.5.2" kstore = "0.9.1" appdirs = "1.2.2" napier = "2.7.1" +androidx-navigation-compose = "2.9.0-alpha15" [libraries] @@ -24,10 +24,9 @@ androidx-activityCompose = { module = "androidx.activity:activity-compose", vers androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "viewmodel-compose" } androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation-compose" } androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uitest" } androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uitest" } -voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } -voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } coil = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" } coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }