add bottom navigation bar, settings screen, etc
This commit is contained in:
@@ -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<KotlinCompile>().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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+2
-2
@@ -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<AppSettings> {
|
||||
return storeOf(file = Path("$appDir/app_settings.json"))
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package dev.meloda.overseerr.model
|
||||
|
||||
internal actual class Platform actual constructor() {
|
||||
actual val name: String
|
||||
get() = "Android"
|
||||
}
|
||||
+1
-1
@@ -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.*
|
||||
@@ -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> {
|
||||
MainScreen(navController)
|
||||
}
|
||||
|
||||
composable<LoginScreen> {
|
||||
LoginScreen(onBack = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<RequestsScreen> {
|
||||
RequestsScreen(onBack = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<UrlScreen> {
|
||||
UrlScreen(onBack = navController::popBackStack)
|
||||
}
|
||||
}
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
+2
-2
@@ -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<AppSettings>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+4
-4
@@ -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()
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.meloda.overseerr.settings.model
|
||||
package dev.meloda.overseerr.datastore.model
|
||||
|
||||
enum class ThemeMode {
|
||||
System, Dark, Light
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package dev.meloda.overseerr.model
|
||||
|
||||
internal expect class Platform() {
|
||||
val name: String
|
||||
}
|
||||
+2
-2
@@ -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<*>
|
||||
@@ -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.*
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+2
-18
@@ -32,7 +32,7 @@ data object LoginScreen
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginScreen(onBack: () -> Unit = {}) {
|
||||
fun LoginScreen() {
|
||||
val viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||
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),
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
.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
|
||||
) {
|
||||
Button(
|
||||
onClick = { navController.navigate(UrlScreen) },
|
||||
modifier = Modifier.weight(0.3f)
|
||||
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)
|
||||
) {
|
||||
Text(text = "Url")
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Text(text = "Search Movies & TV")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { navController.navigate(LoginScreen) },
|
||||
modifier = Modifier.weight(0.3f)
|
||||
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
|
||||
) {
|
||||
Text(text = "Login")
|
||||
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 ->
|
||||
CompositionLocalProvider(
|
||||
LocalHazeState provides hazeState,
|
||||
LocalPadding provides padding
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = HomeScreen,
|
||||
enterTransition = { fadeIn(animationSpec = tween(0)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(0)) }
|
||||
) {
|
||||
composable<HomeScreen> {
|
||||
HomeScreen()
|
||||
}
|
||||
composable<LoginScreen> {
|
||||
LoginScreen()
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { navController.navigate(RequestsScreen) },
|
||||
modifier = Modifier.weight(0.3f)
|
||||
) {
|
||||
Text(text = "Requests")
|
||||
composable<RequestsScreen> {
|
||||
RequestsScreen()
|
||||
}
|
||||
|
||||
composable<SettingsScreen> {
|
||||
SettingsScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NavigationItem(
|
||||
val text: String,
|
||||
val icon: ImageVector,
|
||||
val route: Any,
|
||||
)
|
||||
|
||||
+4
-4
@@ -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,
|
||||
)
|
||||
|
||||
+12
-72
@@ -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<RequestsViewModelImpl>()
|
||||
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,46 +61,7 @@ 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(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -117,7 +73,7 @@ fun RequestsScreen(
|
||||
)
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
Spacer(modifier = Modifier.height(LocalPadding.current.calculateTopPadding()))
|
||||
}
|
||||
item {
|
||||
AnimatedVisibility(screenState.apiErrorText != null || screenState.apiInfo != null) {
|
||||
@@ -147,30 +103,14 @@ fun RequestsScreen(
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(bottomPadding))
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = padding.calculateTopPadding())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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<SettingsScreenState> = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||
val screenState: StateFlow<SettingsScreenState> = _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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -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)
|
||||
}
|
||||
+4
-4
@@ -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
|
||||
+139
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<UrlScreenState>
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
-108
@@ -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<UrlViewModelImpl>()
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
+3
-2
@@ -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<AppSettings> {
|
||||
return storeOf(file = Path("$appDir/app_settings.json"))
|
||||
return storeOf(file = Path("${appDir}/app_settings.json"))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package dev.meloda.overseerr.model
|
||||
|
||||
internal actual class Platform actual constructor() {
|
||||
actual val name: String
|
||||
get() = "JVM"
|
||||
}
|
||||
+1
-1
@@ -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.*
|
||||
@@ -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 {
|
||||
LaunchedEffect(Unit) {
|
||||
appDir = AppDirsFactory.getInstance()
|
||||
.getUserDataDir("Overseerr-KMP", "1.0.0", "dev.meloda")
|
||||
|
||||
println("appDir: $appDir")
|
||||
|
||||
Napier.d("appDir: $appDir")
|
||||
File(appDir).mkdirs()
|
||||
|
||||
startKoin {
|
||||
modules(appModule)
|
||||
}
|
||||
|
||||
Window(
|
||||
@@ -27,7 +26,10 @@ fun main() = application {
|
||||
state = rememberWindowState(width = 800.dp, height = 600.dp),
|
||||
onCloseRequest = ::exitApplication
|
||||
) {
|
||||
window.minimumSize = Dimension(360, 600)
|
||||
window.minimumSize = Dimension(320, 480)
|
||||
|
||||
KoinApplication(application = { modules(appModule) }) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package dev.meloda.overseerr.model
|
||||
|
||||
internal actual class Platform actual constructor() {
|
||||
actual val name: String = "JS"
|
||||
}
|
||||
+1
-1
@@ -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.*
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user