From 5aea08ffd49633539101f2deaecd3842373aaa38 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 28 Sep 2025 21:21:30 +0300 Subject: [PATCH] add bottom navigation bar, settings screen, etc --- composeApp/build.gradle.kts | 13 ++ .../dev/meloda/overseerr/App.android.kt | 8 - .../SettingsStoreProvider.android.kt | 4 +- .../overseerr/model/Platform.android.kt | 6 - ...ttpClientEngineFactoryProvider.android.kt} | 2 +- .../kotlin/dev/meloda/overseerr/App.kt | 37 +-- .../SettingsController.kt | 6 +- .../SettingsStoreProvider.kt | 4 +- .../overseerr/datastore/di/DataStoreModule.kt | 13 ++ .../model/AppSettings.kt | 8 +- .../model/ThemeMode.kt | 2 +- .../dev/meloda/overseerr/di/AppModule.kt | 12 +- .../dev/meloda/overseerr/model/Platform.kt | 5 - .../HttpClientEngineFactoryProvider.kt | 4 +- .../overseerr/network/di/NetworkModule.kt | 2 +- .../overseerr/screens/home/HomeScreen.kt | 22 ++ .../screens/login/presentation/LoginScreen.kt | 22 +- .../overseerr/screens/main/MainScreen.kt | 221 ++++++++++++------ .../screens/requests/RequestsViewModel.kt | 8 +- .../requests/presentation/RequestsScreen.kt | 162 ++++--------- .../screens/settings/SettingsViewModel.kt | 72 ++++++ .../screens/settings/di/SettingsModule.kt | 9 + .../model/SettingsScreenState.kt} | 8 +- .../settings/presentation/SettingsScreen.kt | 139 +++++++++++ .../overseerr/screens/url/UrlViewModel.kt | 79 ------- .../overseerr/screens/url/di/UrlModule.kt | 9 - .../screens/url/presentation/UrlScreen.kt | 108 --------- .../overseerr/settings/di/SettingsModule.kt | 13 -- .../dev/meloda/overseerr/theme/Theme.kt | 10 +- .../SettingsStoreProvider.jvm.kt | 5 +- .../meloda/overseerr/model/Platform.jvm.kt | 6 - .../HttpClientEngineFactoryProvider.jvm.kt} | 2 +- composeApp/src/jvmMain/kotlin/main.kt | 24 +- .../SettingsStoreProvider.wasmJs.kt | 3 +- .../meloda/overseerr/model/Platform.wasmJs.kt | 5 - .../HttpClientEngineFactoryProvider.wasmJs.kt | 2 +- composeApp/src/wasmJsMain/kotlin/main.kt | 7 +- gradle.properties | 4 +- 38 files changed, 538 insertions(+), 528 deletions(-) rename composeApp/src/androidMain/kotlin/dev/meloda/overseerr/{settings/model => datastore}/SettingsStoreProvider.android.kt (77%) delete mode 100644 composeApp/src/androidMain/kotlin/dev/meloda/overseerr/model/Platform.android.kt rename composeApp/src/{jvmMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.jvm.kt => androidMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.android.kt} (82%) rename composeApp/src/commonMain/kotlin/dev/meloda/overseerr/{settings => datastore}/SettingsController.kt (88%) rename composeApp/src/commonMain/kotlin/dev/meloda/overseerr/{settings/model => datastore}/SettingsStoreProvider.kt (56%) create mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/di/DataStoreModule.kt rename composeApp/src/commonMain/kotlin/dev/meloda/overseerr/{settings => datastore}/model/AppSettings.kt (50%) rename composeApp/src/commonMain/kotlin/dev/meloda/overseerr/{settings => datastore}/model/ThemeMode.kt (52%) delete mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt rename composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/{model => }/HttpClientEngineFactoryProvider.kt (51%) create mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/home/HomeScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/SettingsViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/di/SettingsModule.kt rename composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/{url/model/UrlScreenState.kt => settings/model/SettingsScreenState.kt} (50%) create mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/presentation/SettingsScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/UrlViewModel.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/di/UrlModule.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/di/SettingsModule.kt rename composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/{settings/model => datastore}/SettingsStoreProvider.jvm.kt (62%) delete mode 100644 composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/model/Platform.jvm.kt rename composeApp/src/{androidMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.android.kt => jvmMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.jvm.kt} (82%) rename composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/{settings/model => datastore}/SettingsStoreProvider.wasmJs.kt (70%) delete mode 100644 composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/model/Platform.wasmJs.kt rename composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/{model => }/HttpClientEngineFactoryProvider.wasmJs.kt (81%) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 6996690..77349f5 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.util.* plugins { @@ -14,6 +15,17 @@ plugins { alias(libs.plugins.kotlinx.serialization) } +tasks.withType().configureEach { + compilerOptions { + freeCompilerArgs.addAll( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.FlowPreview", + "-Xexpect-actual-classes" + ) + } +} + kotlin { androidTarget { compilerOptions { @@ -172,6 +184,7 @@ android { } getByName("release") { signingConfig = signingConfigs.getByName("release") +// signingConfig = signingConfigs.getByName("debug") isMinifyEnabled = true isShrinkResources = true diff --git a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/App.android.kt b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/App.android.kt index 064d01a..02c1b73 100644 --- a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/App.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/App.android.kt @@ -4,8 +4,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview class AppActivity : ComponentActivity() { @@ -16,9 +14,3 @@ class AppActivity : ComponentActivity() { setContent { App() } } } - -@Preview -@Composable -fun AppPreview() { - App() -} diff --git a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.android.kt b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.android.kt similarity index 77% rename from composeApp/src/androidMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.android.kt rename to composeApp/src/androidMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.android.kt index 8053a3b..fef369a 100644 --- a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.android.kt @@ -1,12 +1,12 @@ -package dev.meloda.overseerr.settings.model +package dev.meloda.overseerr.datastore import dev.meloda.overseerr.appDir +import dev.meloda.overseerr.datastore.model.AppSettings import io.github.xxfast.kstore.KStore import io.github.xxfast.kstore.file.storeOf import kotlinx.io.files.Path actual class SettingsStoreProvider actual constructor() { - actual fun provideStore(): KStore { return storeOf(file = Path("$appDir/app_settings.json")) } 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 deleted file mode 100644 index b35f2b1..0000000 --- a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/model/Platform.android.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.meloda.overseerr.model - -internal actual class Platform actual constructor() { - actual val name: String - get() = "Android" -} diff --git a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.jvm.kt b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.android.kt similarity index 82% rename from composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.jvm.kt rename to composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.android.kt index b11a66c..f986ac3 100644 --- a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.jvm.kt +++ b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.android.kt @@ -1,4 +1,4 @@ -package dev.meloda.overseerr.network.model +package dev.meloda.overseerr.network import io.ktor.client.engine.* import io.ktor.client.engine.okhttp.* diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt index 7c350ae..37f5c2c 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt @@ -7,25 +7,17 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -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.datastore.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 import org.koin.compose.koinInject var appDir: String = "" @Composable -internal fun App() = KoinContext { +internal fun App() { LaunchedEffect(true) { Napier.base(DebugAntilog()) } @@ -37,32 +29,9 @@ internal fun App() = KoinContext { settingsController.loadAppSettings() } - val navController = rememberNavController() - - NavigationSettings(navController) - AppTheme(themeMode = settings.themeMode) { Surface(modifier = Modifier.fillMaxSize()) { - NavHost( - navController = navController, - startDestination = MainScreen - ) { - composable { - MainScreen(navController) - } - - composable { - LoginScreen(onBack = navController::popBackStack) - } - - composable { - RequestsScreen(onBack = navController::popBackStack) - } - - composable { - UrlScreen(onBack = navController::popBackStack) - } - } + MainScreen() } } } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/SettingsController.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/SettingsController.kt similarity index 88% rename from composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/SettingsController.kt rename to composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/SettingsController.kt index f748a2d..a37060d 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/SettingsController.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/SettingsController.kt @@ -1,8 +1,8 @@ -package dev.meloda.overseerr.settings +package dev.meloda.overseerr.datastore import dev.meloda.overseerr.ext.setValue -import dev.meloda.overseerr.settings.model.AppSettings -import dev.meloda.overseerr.settings.model.ThemeMode +import dev.meloda.overseerr.datastore.model.AppSettings +import dev.meloda.overseerr.datastore.model.ThemeMode import io.github.xxfast.kstore.KStore import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.kt similarity index 56% rename from composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.kt rename to composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.kt index 15c690f..2b0928d 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.kt @@ -1,8 +1,8 @@ -package dev.meloda.overseerr.settings.model +package dev.meloda.overseerr.datastore +import dev.meloda.overseerr.datastore.model.AppSettings import io.github.xxfast.kstore.KStore expect class SettingsStoreProvider() { - fun provideStore(): KStore } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/di/DataStoreModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/di/DataStoreModule.kt new file mode 100644 index 0000000..d85a062 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/di/DataStoreModule.kt @@ -0,0 +1,13 @@ +package dev.meloda.overseerr.datastore.di + +import dev.meloda.overseerr.datastore.SettingsController +import dev.meloda.overseerr.datastore.SettingsControllerImpl +import dev.meloda.overseerr.datastore.SettingsStoreProvider +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val dataStoreModule = module { + single { SettingsStoreProvider().provideStore() } + singleOf(::SettingsControllerImpl) bind SettingsController::class +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/model/AppSettings.kt similarity index 50% rename from composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt rename to composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/model/AppSettings.kt index 26f916c..0b148b4 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/model/AppSettings.kt @@ -1,12 +1,12 @@ -package dev.meloda.overseerr.settings.model +package dev.meloda.overseerr.datastore.model import kotlinx.serialization.Serializable @Serializable data class AppSettings( - val url: String = "", - val plexToken: String = "", - val themeMode: ThemeMode = ThemeMode.System + val url: String? = null, + val plexToken: String? = null, + val themeMode: ThemeMode = ThemeMode.System, ) { companion object { val EMPTY: AppSettings = AppSettings() diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/ThemeMode.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/model/ThemeMode.kt similarity index 52% rename from composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/ThemeMode.kt rename to composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/model/ThemeMode.kt index 6997bda..96bbda7 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/ThemeMode.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/datastore/model/ThemeMode.kt @@ -1,4 +1,4 @@ -package dev.meloda.overseerr.settings.model +package dev.meloda.overseerr.datastore.model enum class ThemeMode { System, Dark, Light diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/di/AppModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/di/AppModule.kt index dca5ea1..a4a68d6 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/di/AppModule.kt @@ -1,22 +1,18 @@ package dev.meloda.overseerr.di -import dev.meloda.overseerr.model.Platform +import dev.meloda.overseerr.datastore.di.dataStoreModule import dev.meloda.overseerr.network.di.networkModule import dev.meloda.overseerr.screens.login.di.loginModule import dev.meloda.overseerr.screens.requests.di.requestsModule -import dev.meloda.overseerr.screens.url.di.urlModule -import dev.meloda.overseerr.settings.di.settingsModule -import org.koin.core.module.dsl.singleOf +import dev.meloda.overseerr.screens.settings.di.settingsModule import org.koin.dsl.module val appModule = module { - singleOf(::Platform) - includes( - settingsModule, + dataStoreModule, networkModule, loginModule, - urlModule, + settingsModule, requestsModule ) } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt deleted file mode 100644 index 28919d0..0000000 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/model/Platform.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.meloda.overseerr.model - -internal expect class Platform() { - val name: String -} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.kt similarity index 51% rename from composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.kt rename to composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.kt index 0c828d8..4a7e981 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.kt @@ -1,6 +1,6 @@ -package dev.meloda.overseerr.network.model +package dev.meloda.overseerr.network -import io.ktor.client.engine.* +import io.ktor.client.engine.HttpClientEngineFactory expect class HttpClientEngineFactoryProvider() { fun get(): HttpClientEngineFactory<*> diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/di/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/di/NetworkModule.kt index d9d4c92..c16900d 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/di/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/di/NetworkModule.kt @@ -1,6 +1,6 @@ package dev.meloda.overseerr.network.di -import dev.meloda.overseerr.network.model.HttpClientEngineFactoryProvider +import dev.meloda.overseerr.network.HttpClientEngineFactoryProvider import io.ktor.client.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/home/HomeScreen.kt new file mode 100644 index 0000000..29204c9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/home/HomeScreen.kt @@ -0,0 +1,22 @@ +package dev.meloda.overseerr.screens.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import kotlinx.serialization.Serializable + +@Serializable +data object HomeScreen + +@Composable +fun HomeScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Home Screen") + } +} 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 f4743e1..1cf1e8c 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 @@ -32,7 +32,7 @@ data object LoginScreen @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LoginScreen(onBack: () -> Unit = {}) { +fun LoginScreen() { val viewModel: LoginViewModel = koinViewModel() val screenState: LoginScreenState by viewModel.screenState.collectAsStateWithLifecycle() @@ -43,23 +43,7 @@ fun LoginScreen(onBack: () -> Unit = {}) { mutableStateOf(screenState.password) } - Scaffold( - topBar = { - TopAppBar( - title = { - Text(text = "Log in") - }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = null - ) - } - } - ) - } - ) { padding -> + Scaffold(topBar = { TopAppBar(title = { Text(text = "Log in") }) }) { padding -> Column( modifier = Modifier.fillMaxSize() .padding(padding), @@ -129,4 +113,4 @@ fun LoginScreen(onBack: () -> Unit = {}) { } } } -} \ 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 54208c0..7f57400 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 @@ -1,91 +1,178 @@ package dev.meloda.overseerr.screens.main -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material.icons.rounded.Timer import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import coil3.ImageLoader +import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.overseerr.screens.home.HomeScreen 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 +import dev.meloda.overseerr.screens.settings.presentation.SettingsScreen +import dev.meloda.overseerr.theme.LocalHazeState +import dev.meloda.overseerr.theme.LocalPadding -@Serializable -data object MainScreen - -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable -fun MainScreen(navController: NavController) { - val coroutineScope = rememberCoroutineScope() +fun MainScreen() { + val hazeState = remember { HazeState(true) } - val settingsController: SettingsController = koinInject() - val settings by settingsController.settings.collectAsStateWithLifecycle() + val navController = rememberNavController() + + val navigationItems = remember { + listOf( + NavigationItem("Home", Icons.Rounded.Home, HomeScreen), + NavigationItem("Requests", Icons.Rounded.Timer, RequestsScreen), + NavigationItem("Settings", Icons.Rounded.Settings, SettingsScreen), + ) + } + + var selectedItemIndex by rememberSaveable { + mutableStateOf(navigationItems.indexOfFirst { it.route == HomeScreen }) + } 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() - } - } - ) { - Text( - text = when (settings.themeMode) { - ThemeMode.System -> "System" - ThemeMode.Dark -> "Dark" - ThemeMode.Light -> "Light" - } + Row( + modifier = Modifier + .height(64.dp) + .fillMaxWidth() + .hazeEffect( + state = hazeState, + style = HazeMaterials.regular(NavigationBarDefaults.containerColor) + ) + .windowInsetsPadding(TopAppBarDefaults.windowInsets) + .padding( + horizontal = 8.dp, + vertical = 10.dp + ), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + modifier = Modifier + .height(42.dp) + .weight(1f) + .clip(RoundedCornerShape(50)) + .border( + width = 1.dp, + color = Color.DarkGray.copy(alpha = 0.5f), + shape = RoundedCornerShape(50) ) - } + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f)) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = null + ) + + Text(text = "Search Movies & TV") } - ) + + AsyncImage( + modifier = Modifier.clip(CircleShape).size(32.dp), + model = ImageRequest.Builder(LocalPlatformContext.current) + .data("https://assets.plex.tv/avatars/f812cd60749d3776cba6c6e31cdc3bcee8c5f5d9.?1707518880") + .size(64) + .build(), + contentDescription = null, + imageLoader = ImageLoader.Builder(LocalPlatformContext.current).build(), + ) + } + }, + bottomBar = { + NavigationBar( + modifier = Modifier + .fillMaxWidth() + .hazeEffect( + state = hazeState, + style = HazeMaterials.regular(NavigationBarDefaults.containerColor) + ), + containerColor = Color.Transparent + ) { + navigationItems.forEachIndexed { index, item -> + NavigationBarItem( + selected = index == selectedItemIndex, + onClick = { + if (selectedItemIndex != index) { + val currentRoute = navigationItems[selectedItemIndex].route + selectedItemIndex = index + navController.navigate(item.route) { + popUpTo(route = currentRoute) { + inclusive = true + } + } + } + }, + icon = { Icon(imageVector = item.icon, contentDescription = null) }, + label = { Text(text = item.text) } + ) + } + } } ) { padding -> - Row( - modifier = Modifier - .padding(padding) - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), + CompositionLocalProvider( + LocalHazeState provides hazeState, + LocalPadding provides padding ) { - Button( - onClick = { navController.navigate(UrlScreen) }, - modifier = Modifier.weight(0.3f) + NavHost( + navController = navController, + startDestination = HomeScreen, + enterTransition = { fadeIn(animationSpec = tween(0)) }, + exitTransition = { fadeOut(animationSpec = tween(0)) } ) { - Text(text = "Url") - } + composable { + HomeScreen() + } + composable { + LoginScreen() + } - Button( - onClick = { navController.navigate(LoginScreen) }, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Login") - } + composable { + RequestsScreen() + } - Button( - onClick = { navController.navigate(RequestsScreen) }, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Requests") + composable { + SettingsScreen() + } } } } -} \ No newline at end of file +} + +data class NavigationItem( + val text: String, + val icon: ImageVector, + val route: Any, +) diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsViewModel.kt index 65f1ed2..5a4b734 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.meloda.overseerr.ext.setValue import dev.meloda.overseerr.screens.requests.model.RequestsScreenState -import dev.meloda.overseerr.settings.SettingsController +import dev.meloda.overseerr.datastore.SettingsController import io.github.aakira.napier.Napier import io.ktor.client.* import io.ktor.client.call.* @@ -27,7 +27,7 @@ interface RequestsViewModel { class RequestsViewModelImpl( private val httpClient: HttpClient, - private val settingsController: SettingsController + private val settingsController: SettingsController, ) : ViewModel(), RequestsViewModel { override val screenState = MutableStateFlow(RequestsScreenState.EMPTY) @@ -54,7 +54,7 @@ class RequestsViewModelImpl( kotlin.runCatching { httpClient.get("${settingsController.settings.value.url}/api/v1") { headers { - append("X-Api-Key", settingsController.settings.value.plexToken) + append("X-Api-Key", settingsController.settings.value.plexToken.orEmpty()) } }.body() as ApiInfo }.fold( @@ -74,5 +74,5 @@ class RequestsViewModelImpl( @Serializable data class ApiInfo( val api: String, - val version: String + val version: String, ) 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 c671787..b67ca85 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 @@ -5,31 +5,28 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState 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 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 dev.meloda.overseerr.theme.LocalHazeState +import dev.meloda.overseerr.theme.LocalPadding import kotlinx.coroutines.delay import kotlinx.serialization.Serializable import org.koin.compose.viewmodel.koinViewModel @@ -43,14 +40,12 @@ data object RequestsScreen ExperimentalHazeMaterialsApi::class ) @Composable -fun RequestsScreen( - onBack: () -> Unit = {} -) { +fun RequestsScreen() { val viewModel: RequestsViewModel = koinViewModel() val screenState: RequestsScreenState by viewModel.screenState.collectAsStateWithLifecycle() - val hazeState = remember { HazeState() } - val hazeStyle = HazeMaterials.ultraThin() + val padding = LocalPadding.current + val hazeState = LocalHazeState.current val refreshState = rememberPullToRefreshState() @@ -66,111 +61,56 @@ fun RequestsScreen( } } - 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( + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( modifier = Modifier .fillMaxSize() - .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) - .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) + .hazeSource(state = hazeState) + .pullToRefresh( + isRefreshing = screenState.isLoading, + state = refreshState, + onRefresh = viewModel::onRefresh + ) ) { - 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) - ) - } + item { + Spacer(modifier = Modifier.height(LocalPadding.current.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()) - ) - - if (bottomPadding.value > 0) { - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .hazeEffect( - state = hazeState, - style = hazeStyle - ) - .background(Color.Transparent) - .height(bottomPadding) - .fillMaxWidth() + 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(padding.calculateBottomPadding())) } } + + Indicator( + state = refreshState, + isRefreshing = screenState.isLoading, + modifier = Modifier.align(Alignment.TopCenter).padding(top = padding.calculateTopPadding()) + ) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/SettingsViewModel.kt new file mode 100644 index 0000000..791c646 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/SettingsViewModel.kt @@ -0,0 +1,72 @@ +package dev.meloda.overseerr.screens.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dev.meloda.overseerr.datastore.SettingsController +import dev.meloda.overseerr.ext.setValue +import dev.meloda.overseerr.screens.settings.model.SettingsScreenState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SettingsViewModel(private val settingsController: SettingsController) : ViewModel() { + + private val _screenState: MutableStateFlow = MutableStateFlow(SettingsScreenState.EMPTY) + val screenState: StateFlow = _screenState.asStateFlow() + + init { + settingsController.settings + .onEach { settings -> + _screenState.emit( + screenState.value.copy( + url = settings.url.orEmpty(), + plexToken = settings.plexToken.orEmpty() + ) + ) + } + .launchIn(viewModelScope) + } + + fun onScreenOpened() { + _screenState.setValue { old -> + old.copy( + url = settingsController.settings.value.url.orEmpty(), + plexToken = settingsController.settings.value.plexToken.orEmpty() + ) + } + } + + fun onUrlInputChanged(newText: String) { + _screenState.setValue { old -> old.copy(url = newText) } + } + + fun onPlexTokenInputChanged(newToken: String) { + _screenState.setValue { old -> old.copy(plexToken = newToken) } + } + + fun onLoadButtonClicked() { + viewModelScope.launch { + val settings = settingsController.loadAppSettings() + withContext(Dispatchers.Main) { + _screenState.emit( + screenState.value.copy( + url = settings.url.orEmpty(), + plexToken = settings.plexToken.orEmpty() + ) + ) + } + } + } + + fun onSaveButtonClicked() { + viewModelScope.launch { + settingsController.updateAppSettings { settings -> + settings.copy( + url = screenState.value.url.trim().ifEmpty { null }, + plexToken = screenState.value.plexToken.trim().ifEmpty { null } + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/di/SettingsModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/di/SettingsModule.kt new file mode 100644 index 0000000..47b5d0d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/di/SettingsModule.kt @@ -0,0 +1,9 @@ +package dev.meloda.overseerr.screens.settings.di + +import dev.meloda.overseerr.screens.settings.SettingsViewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val settingsModule = module { + viewModelOf(::SettingsViewModel) +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/model/UrlScreenState.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/model/SettingsScreenState.kt similarity index 50% rename from composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/model/UrlScreenState.kt rename to composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/model/SettingsScreenState.kt index b74ed12..228a77a 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/model/UrlScreenState.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/model/SettingsScreenState.kt @@ -1,12 +1,12 @@ -package dev.meloda.overseerr.screens.url.model +package dev.meloda.overseerr.screens.settings.model -data class UrlScreenState( +data class SettingsScreenState( val url: String, val plexToken: String, - val isWrongUrlError: Boolean + val isWrongUrlError: Boolean, ) { companion object { - val EMPTY: UrlScreenState = UrlScreenState( + val EMPTY: SettingsScreenState = SettingsScreenState( url = "", plexToken = "", isWrongUrlError = false diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/presentation/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/presentation/SettingsScreen.kt new file mode 100644 index 0000000..40be9fe --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/settings/presentation/SettingsScreen.kt @@ -0,0 +1,139 @@ +package dev.meloda.overseerr.screens.settings.presentation + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 dev.meloda.overseerr.datastore.SettingsController +import dev.meloda.overseerr.datastore.model.ThemeMode +import dev.meloda.overseerr.screens.settings.SettingsViewModel +import dev.meloda.overseerr.theme.LocalPadding +import kotlinx.serialization.Serializable +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel + +@Serializable +data object SettingsScreen + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen() { + val viewModel: SettingsViewModel = koinViewModel() + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + + val settingsController: SettingsController = koinInject() + val settings by settingsController.settings.collectAsStateWithLifecycle() + + LaunchedEffect(true) { + viewModel.onScreenOpened() + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(LocalPadding.current) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + var localUrl by rememberSaveable(screenState.url) { + mutableStateOf(screenState.url) + } + var localPlexToken by rememberSaveable(screenState.plexToken) { + mutableStateOf(screenState.plexToken) + } + + Text( + text = "Dark mode", + style = MaterialTheme.typography.titleMedium + ) + + SingleChoiceSegmentedButtonRow { + SegmentedButton( + selected = settings.themeMode == ThemeMode.Light, + onClick = { settingsController.updateThemeMode(ThemeMode.Light) }, + shape = RoundedCornerShape( + topStart = 4.dp, + bottomStart = 4.dp + ), + label = { Text(text = "Light") } + ) + + SegmentedButton( + selected = settings.themeMode == ThemeMode.Dark, + onClick = { settingsController.updateThemeMode(ThemeMode.Dark) }, + shape = RoundedCornerShape(0.dp), + label = { Text(text = "Dark") } + ) + + SegmentedButton( + selected = settings.themeMode == ThemeMode.System, + onClick = { settingsController.updateThemeMode(ThemeMode.System) }, + shape = RoundedCornerShape( + topEnd = 4.dp, + bottomEnd = 4.dp + ), + label = { Text(text = "System") } + ) + } + + Text( + text = "API", + style = MaterialTheme.typography.titleMedium + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = localUrl, + onValueChange = { text -> + localUrl = text + viewModel.onUrlInputChanged(text) + }, + placeholder = { Text(text = "Url") }, + isError = screenState.isWrongUrlError, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Go, + keyboardType = KeyboardType.Uri + ) + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = localPlexToken, + onValueChange = { text -> + localPlexToken = text + viewModel.onPlexTokenInputChanged(text) + }, + placeholder = { Text(text = "Token") }, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Go) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Button( + onClick = viewModel::onSaveButtonClicked, + modifier = Modifier.weight(1f) + ) { + Text(text = "Save") + } + + Button( + onClick = viewModel::onLoadButtonClicked, + modifier = Modifier.weight(1f) + ) { + Text(text = "Load") + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/UrlViewModel.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/UrlViewModel.kt deleted file mode 100644 index d0bc9e1..0000000 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/UrlViewModel.kt +++ /dev/null @@ -1,79 +0,0 @@ -package dev.meloda.overseerr.screens.url - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dev.meloda.overseerr.ext.setValue -import dev.meloda.overseerr.screens.url.model.UrlScreenState -import dev.meloda.overseerr.settings.SettingsController -import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -interface UrlViewModel { - val screenState: StateFlow - - fun onUrlInputChanged(newText: String) - fun onPlexTokenInputChanged(newToken: String) - fun onLoadButtonClicked() - fun onSaveButtonClicked() - fun onTestButtonClicked() -} - -class UrlViewModelImpl( - private val settingsController: SettingsController -) : ViewModel(), UrlViewModel { - - override val screenState = MutableStateFlow(UrlScreenState.EMPTY) - - init { - settingsController.settings - .onEach { settings -> - screenState.setValue { old -> - old.copy( - url = settings.url, - plexToken = settings.plexToken - ) - } - } - .launchIn(viewModelScope) - } - - override fun onUrlInputChanged(newText: String) { - screenState.setValue { old -> old.copy(url = newText) } - } - - override fun onPlexTokenInputChanged(newToken: String) { - screenState.setValue { old -> old.copy(plexToken = newToken) } - } - - override fun onLoadButtonClicked() { - viewModelScope.launch { - val settings = settingsController.loadAppSettings() - - screenState.setValue { old -> - old.copy( - url = settings.url, - plexToken = settings.plexToken - ) - } - } - } - - override fun onSaveButtonClicked() { - viewModelScope.launch { - settingsController.updateAppSettings { settings -> - settings.copy( - url = screenState.value.url, - plexToken = screenState.value.plexToken - ) - } - } - } - - override fun onTestButtonClicked() { - Napier.v("Test button clicked") - } -} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/di/UrlModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/di/UrlModule.kt deleted file mode 100644 index 9ec4824..0000000 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/di/UrlModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.meloda.overseerr.screens.url.di - -import dev.meloda.overseerr.screens.url.UrlViewModelImpl -import org.koin.core.module.dsl.viewModelOf -import org.koin.dsl.module - -val urlModule = module { - viewModelOf(::UrlViewModelImpl) -} 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 deleted file mode 100644 index b64f540..0000000 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt +++ /dev/null @@ -1,108 +0,0 @@ -package dev.meloda.overseerr.screens.url.presentation - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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 dev.meloda.overseerr.screens.url.UrlViewModel -import dev.meloda.overseerr.screens.url.UrlViewModelImpl -import kotlinx.serialization.Serializable -import org.koin.compose.viewmodel.koinViewModel - -@Serializable -data object UrlScreen - -@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 = 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 - ) - ) - - 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) - ) { - Text(text = "Load") - } - - Button( - onClick = viewModel::onSaveButtonClicked, - modifier = Modifier.weight(0.3f) - ) { - Text(text = "Save") - } - - 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/settings/di/SettingsModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/di/SettingsModule.kt deleted file mode 100644 index 3e71e38..0000000 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/di/SettingsModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.meloda.overseerr.settings.di - -import dev.meloda.overseerr.settings.SettingsController -import dev.meloda.overseerr.settings.SettingsControllerImpl -import dev.meloda.overseerr.settings.model.SettingsStoreProvider -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.bind -import org.koin.dsl.module - -val settingsModule = module { - single { SettingsStoreProvider().provideStore() } - singleOf(::SettingsControllerImpl) bind SettingsController::class -} 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 05f9164..9eae9a9 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/theme/Theme.kt @@ -1,20 +1,24 @@ package dev.meloda.overseerr.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.MaterialTheme 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 +import dev.chrisbanes.haze.HazeState +import dev.meloda.overseerr.datastore.model.ThemeMode -internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } +val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } +val LocalHazeState = compositionLocalOf { HazeState(true) } +val LocalPadding = compositionLocalOf { PaddingValues() } @Composable internal fun AppTheme( themeMode: ThemeMode = ThemeMode.System, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val systemIsDark = isSystemInDarkTheme() val isDarkState = remember(themeMode, systemIsDark) { diff --git a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.jvm.kt similarity index 62% rename from composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.jvm.kt rename to composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.jvm.kt index 0991ef2..bfc5834 100644 --- a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.jvm.kt @@ -1,12 +1,13 @@ -package dev.meloda.overseerr.settings.model +package dev.meloda.overseerr.datastore import dev.meloda.overseerr.appDir +import dev.meloda.overseerr.datastore.model.AppSettings import io.github.xxfast.kstore.KStore import io.github.xxfast.kstore.file.storeOf import kotlinx.io.files.Path actual class SettingsStoreProvider actual constructor() { actual fun provideStore(): KStore { - return storeOf(file = Path("$appDir/app_settings.json")) + return storeOf(file = Path("${appDir}/app_settings.json")) } } 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 deleted file mode 100644 index f4501ab..0000000 --- a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/model/Platform.jvm.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.meloda.overseerr.model - -internal actual class Platform actual constructor() { - actual val name: String - get() = "JVM" -} diff --git a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.android.kt b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.jvm.kt similarity index 82% rename from composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.android.kt rename to composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.jvm.kt index b11a66c..f986ac3 100644 --- a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.android.kt +++ b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.jvm.kt @@ -1,4 +1,4 @@ -package dev.meloda.overseerr.network.model +package dev.meloda.overseerr.network import io.ktor.client.engine.* import io.ktor.client.engine.okhttp.* diff --git a/composeApp/src/jvmMain/kotlin/main.kt b/composeApp/src/jvmMain/kotlin/main.kt index 2af9921..8518130 100644 --- a/composeApp/src/jvmMain/kotlin/main.kt +++ b/composeApp/src/jvmMain/kotlin/main.kt @@ -1,3 +1,4 @@ +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application @@ -5,21 +6,19 @@ import androidx.compose.ui.window.rememberWindowState import dev.meloda.overseerr.App import dev.meloda.overseerr.appDir import dev.meloda.overseerr.di.appModule +import io.github.aakira.napier.Napier import net.harawata.appdirs.AppDirsFactory -import org.koin.core.context.startKoin +import org.koin.compose.KoinApplication import java.awt.Dimension import java.io.File fun main() = application { - appDir = AppDirsFactory.getInstance() - .getUserDataDir("Overseerr-KMP", "1.0.0", "dev.meloda") + LaunchedEffect(Unit) { + appDir = AppDirsFactory.getInstance() + .getUserDataDir("Overseerr-KMP", "1.0.0", "dev.meloda") - println("appDir: $appDir") - - File(appDir).mkdirs() - - startKoin { - modules(appModule) + Napier.d("appDir: $appDir") + File(appDir).mkdirs() } Window( @@ -27,7 +26,10 @@ fun main() = application { state = rememberWindowState(width = 800.dp, height = 600.dp), onCloseRequest = ::exitApplication ) { - window.minimumSize = Dimension(360, 600) - App() + window.minimumSize = Dimension(320, 480) + + KoinApplication(application = { modules(appModule) }) { + App() + } } } diff --git a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.wasmJs.kt similarity index 70% rename from composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.wasmJs.kt rename to composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.wasmJs.kt index 40f720b..b674821 100644 --- a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/settings/model/SettingsStoreProvider.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/datastore/SettingsStoreProvider.wasmJs.kt @@ -1,5 +1,6 @@ -package dev.meloda.overseerr.settings.model +package dev.meloda.overseerr.datastore +import dev.meloda.overseerr.datastore.model.AppSettings import io.github.xxfast.kstore.KStore import io.github.xxfast.kstore.storage.storeOf 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 deleted file mode 100644 index 243d831..0000000 --- a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/model/Platform.wasmJs.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.meloda.overseerr.model - -internal actual class Platform actual constructor() { - actual val name: String = "JS" -} diff --git a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.wasmJs.kt similarity index 81% rename from composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.wasmJs.kt rename to composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.wasmJs.kt index 992d876..db74516 100644 --- a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.wasmJs.kt +++ b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/HttpClientEngineFactoryProvider.wasmJs.kt @@ -1,4 +1,4 @@ -package dev.meloda.overseerr.network.model +package dev.meloda.overseerr.network import io.ktor.client.engine.* import io.ktor.client.engine.js.* diff --git a/composeApp/src/wasmJsMain/kotlin/main.kt b/composeApp/src/wasmJsMain/kotlin/main.kt index 44878ab..8eacc0c 100644 --- a/composeApp/src/wasmJsMain/kotlin/main.kt +++ b/composeApp/src/wasmJsMain/kotlin/main.kt @@ -3,14 +3,13 @@ import androidx.compose.ui.window.ComposeViewport import dev.meloda.overseerr.App import dev.meloda.overseerr.di.appModule import kotlinx.browser.document -import org.koin.core.context.startKoin +import org.koin.compose.KoinApplication @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport(document.body!!) { - startKoin { - modules(appModule) + KoinApplication(application = { modules(appModule) }) { + App() } - App() } } diff --git a/gradle.properties b/gradle.properties index f6a24cb..1f591a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,10 +14,8 @@ kotlin.native.ignoreDisabledTargets=true android.useAndroidX=true android.nonTransitiveRClass=true -#Compose -org.jetbrains.compose.experimental.jscanvas.enabled=true - include_wasm=true include_ios=false + #Flip this to false when including the ios targets org.gradle.unsafe.configuration-cache=true