voyager -> androidx.compose.navigation

This commit is contained in:
2025-03-30 04:32:29 +03:00
parent 8e3d822a56
commit 885ed5a018
17 changed files with 442 additions and 384 deletions
@@ -7,11 +7,16 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.FadeTransition
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import dev.meloda.overseerr.screens.login.presentation.LoginScreen
import dev.meloda.overseerr.screens.main.MainScreen
import dev.meloda.overseerr.screens.requests.presentation.RequestsScreen
import dev.meloda.overseerr.screens.url.presentation.UrlScreen
import dev.meloda.overseerr.settings.SettingsController
import dev.meloda.overseerr.theme.AppTheme
import dev.meloda.overseerr.theme.NavigationSettings
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
import org.koin.compose.KoinContext
@@ -32,10 +37,31 @@ internal fun App() = KoinContext {
settingsController.loadAppSettings()
}
val navController = rememberNavController()
NavigationSettings(navController)
AppTheme(themeMode = settings.themeMode) {
Surface(modifier = Modifier.fillMaxSize()) {
Navigator(MainScreen()) { navigator ->
FadeTransition(navigator)
NavHost(
navController = navController,
startDestination = MainScreen
) {
composable<MainScreen> {
MainScreen(navController)
}
composable<LoginScreen> {
LoginScreen(onBack = navController::popBackStack)
}
composable<RequestsScreen> {
RequestsScreen(onBack = navController::popBackStack)
}
composable<UrlScreen> {
UrlScreen(onBack = navController::popBackStack)
}
}
}
}
@@ -1,5 +1,5 @@
package dev.meloda.overseerr.model
expect class Platform() {
internal expect class Platform() {
val name: String
}
@@ -8,8 +8,11 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
@@ -18,117 +21,112 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.meloda.overseerr.screens.login.LoginViewModel
import dev.meloda.overseerr.screens.login.LoginViewModelImpl
import dev.meloda.overseerr.screens.login.model.LoginScreenState
import kotlinx.serialization.Serializable
import org.koin.compose.viewmodel.koinViewModel
class LoginScreen : Screen {
@Serializable
data object LoginScreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
val screenState: LoginScreenState by viewModel.screenState.collectAsStateWithLifecycle()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(onBack: () -> Unit = {}) {
val viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
val screenState: LoginScreenState by viewModel.screenState.collectAsStateWithLifecycle()
var loginValue by rememberSaveable {
mutableStateOf(screenState.login)
}
var passwordValue by rememberSaveable {
mutableStateOf(screenState.password)
}
var loginValue by rememberSaveable {
mutableStateOf(screenState.login)
}
var passwordValue by rememberSaveable {
mutableStateOf(screenState.password)
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Log in")
},
navigationIcon = {
IconButton(
onClick = navigator::pop
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Log in")
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
)
}
) { padding ->
Column(
modifier = Modifier.fillMaxSize()
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
value = loginValue,
onValueChange = { newText ->
loginValue = newText
viewModel.onLoginInputChanged(newText)
},
placeholder = { Text(text = "Login") },
isError = screenState.isLoginEmptyError,
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
value = passwordValue,
onValueChange = { newText ->
passwordValue = newText
viewModel.onPasswordInputChanged(newText)
},
placeholder = { Text(text = "Password") },
isError = screenState.isPasswordEmptyError,
trailingIcon = {
IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) {
Icon(
imageVector = if (screenState.isPasswordVisible) {
Icons.Rounded.VisibilityOff
} else {
Icons.Rounded.Visibility
},
contentDescription = if (screenState.isPasswordVisible) "Password visible icon"
else "Password invisible icon"
)
}
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Password
),
visualTransformation = if (screenState.isPasswordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
}
) {
Text(text = "Authorize")
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowForward,
contentDescription = null
)
}
)
}
) { padding ->
Column(
modifier = Modifier.fillMaxSize()
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
value = loginValue,
onValueChange = { newText ->
loginValue = newText
viewModel.onLoginInputChanged(newText)
},
placeholder = { Text(text = "Login") },
isError = screenState.isLoginEmptyError,
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
value = passwordValue,
onValueChange = { newText ->
passwordValue = newText
viewModel.onPasswordInputChanged(newText)
},
placeholder = { Text(text = "Password") },
isError = screenState.isPasswordEmptyError,
trailingIcon = {
IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) {
Icon(
imageVector = if (screenState.isPasswordVisible) {
Icons.Rounded.VisibilityOff
} else {
Icons.Rounded.Visibility
},
contentDescription = if (screenState.isPasswordVisible) "Password visible icon"
else "Password invisible icon"
)
}
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Password
),
visualTransformation = if (screenState.isPasswordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
}
) {
Text(text = "Authorize")
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowForward,
contentDescription = null
)
}
}
}
}
}
@@ -10,85 +10,82 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import androidx.navigation.NavController
import dev.meloda.overseerr.screens.login.presentation.LoginScreen
import dev.meloda.overseerr.screens.requests.presentation.RequestsScreen
import dev.meloda.overseerr.screens.url.presentation.UrlScreen
import dev.meloda.overseerr.settings.SettingsController
import dev.meloda.overseerr.settings.model.ThemeMode
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.koin.compose.koinInject
class MainScreen : Screen {
@Serializable
data object MainScreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val coroutineScope = rememberCoroutineScope()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(navController: NavController) {
val coroutineScope = rememberCoroutineScope()
val navigator = LocalNavigator.currentOrThrow
val settingsController: SettingsController = koinInject()
val settings by settingsController.settings.collectAsStateWithLifecycle()
val settingsController: SettingsController = koinInject()
val settings by settingsController.settings.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "Main screen") },
actions = {
TextButton(
onClick = {
val newThemeMode = ThemeMode.entries.getOrElse(
ThemeMode.entries.indexOf(settings.themeMode) + 1
) { ThemeMode.System }
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "Main screen") },
actions = {
TextButton(
onClick = {
val newThemeMode = ThemeMode.entries.getOrElse(
ThemeMode.entries.indexOf(settings.themeMode) + 1
) { ThemeMode.System }
settingsController.updateThemeMode(newThemeMode)
coroutineScope.launch {
settingsController.saveAppSettings()
}
settingsController.updateThemeMode(newThemeMode)
coroutineScope.launch {
settingsController.saveAppSettings()
}
) {
Text(
text = when (settings.themeMode) {
ThemeMode.System -> "System"
ThemeMode.Dark -> "Dark"
ThemeMode.Light -> "Light"
}
)
}
) {
Text(
text = when (settings.themeMode) {
ThemeMode.System -> "System"
ThemeMode.Dark -> "Dark"
ThemeMode.Light -> "Light"
}
)
}
)
}
) { padding ->
Row(
modifier = Modifier
.padding(padding)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
}
)
}
) { padding ->
Row(
modifier = Modifier
.padding(padding)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Button(
onClick = { navController.navigate(UrlScreen) },
modifier = Modifier.weight(0.3f)
) {
Button(
onClick = { navigator.push(UrlScreen()) },
modifier = Modifier.weight(0.3f)
) {
Text(text = "Url")
}
Text(text = "Url")
}
Button(
onClick = { navigator.push(LoginScreen()) },
modifier = Modifier.weight(0.3f)
) {
Text(text = "Login")
}
Button(
onClick = { navController.navigate(LoginScreen) },
modifier = Modifier.weight(0.3f)
) {
Text(text = "Login")
}
Button(
onClick = { navigator.push(RequestsScreen()) },
modifier = Modifier.weight(0.3f)
) {
Text(text = "Requests")
}
Button(
onClick = { navController.navigate(RequestsScreen) },
modifier = Modifier.weight(0.3f)
) {
Text(text = "Requests")
}
}
}
}
}
@@ -12,161 +12,165 @@ import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.chrisbanes.haze.*
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.overseerr.screens.requests.RequestsViewModel
import dev.meloda.overseerr.screens.requests.RequestsViewModelImpl
import dev.meloda.overseerr.screens.requests.model.RequestsScreenState
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import org.koin.compose.viewmodel.koinViewModel
import kotlin.time.Duration.Companion.seconds
class RequestsScreen : Screen {
@Serializable
data object RequestsScreen
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3Api::class
)
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel: RequestsViewModel = koinViewModel<RequestsViewModelImpl>()
val screenState: RequestsScreenState by viewModel.screenState.collectAsStateWithLifecycle()
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun RequestsScreen(
onBack: () -> Unit = {}
) {
val viewModel: RequestsViewModel = koinViewModel<RequestsViewModelImpl>()
val screenState: RequestsScreenState by viewModel.screenState.collectAsStateWithLifecycle()
val hazeState = remember { HazeState() }
val hazeStyle = HazeMaterials.ultraThin()
val hazeState = remember { HazeState() }
val hazeStyle = HazeMaterials.ultraThin()
val refreshState = rememberPullToRefreshState()
val refreshState = rememberPullToRefreshState()
LaunchedEffect(screenState) {
if (screenState.apiInfo != null) {
delay(5.seconds)
viewModel.onSuccessMessageShown()
}
if (screenState.apiErrorText != null) {
delay(5.seconds)
viewModel.onErrorMessageShown()
}
LaunchedEffect(screenState) {
if (screenState.apiInfo != null) {
delay(5.seconds)
viewModel.onSuccessMessageShown()
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text(text = "Requests") },
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
if (screenState.apiErrorText != null) {
delay(5.seconds)
viewModel.onErrorMessageShown()
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text(text = "Requests") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
modifier = Modifier
.hazeEffect(
state = hazeState,
style = hazeStyle
).fillMaxWidth(),
actions = {
IconButton(
onClick = viewModel::onRefresh
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
}
) { padding ->
val bottomPadding = padding.calculateBottomPadding()
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.hazeSource(state = hazeState)
.pullToRefresh(
isRefreshing = screenState.isLoading,
state = refreshState,
onRefresh = viewModel::onRefresh
)
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
item {
AnimatedVisibility(screenState.apiErrorText != null || screenState.apiInfo != null) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
if (screenState.apiInfo != null) Color(0xffb00b69)
else Color.Red
),
contentAlignment = Alignment.CenterStart
) {
Text(
text = screenState.apiErrorText ?: screenState.apiInfo.toString(),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(10.dp)
)
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
}
}
items(items = screenState.dummyItems) { index ->
Text(
text = "Text #${index + 1}",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.background(Color.Red)
)
Spacer(modifier = Modifier.height(64.dp))
}
item {
Spacer(modifier = Modifier.height(bottomPadding))
}
}
Indicator(
state = refreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding())
)
if (bottomPadding.value > 0) {
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.hazeEffect(
state = hazeState,
style = hazeStyle
).fillMaxWidth(),
actions = {
IconButton(
onClick = viewModel::onRefresh
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
}
) { padding ->
val bottomPadding = padding.calculateBottomPadding()
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.hazeSource(state = hazeState)
.pullToRefresh(
isRefreshing = screenState.isLoading,
state = refreshState,
onRefresh = viewModel::onRefresh
)
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
item {
AnimatedVisibility(screenState.apiErrorText != null || screenState.apiInfo != null) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
if (screenState.apiInfo != null) Color(0xffb00b69)
else Color.Red
),
contentAlignment = Alignment.CenterStart
) {
Text(
text = screenState.apiErrorText ?: screenState.apiInfo.toString(),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(10.dp)
)
}
}
}
items(items = screenState.dummyItems) { index ->
Text(
text = "Text #${index + 1}",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.background(Color.Red)
)
Spacer(modifier = Modifier.height(64.dp))
}
item {
Spacer(modifier = Modifier.height(bottomPadding))
}
}
Indicator(
state = refreshState,
isRefreshing = screenState.isLoading,
modifier = Modifier.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding())
.background(Color.Transparent)
.height(bottomPadding)
.fillMaxWidth()
)
if (bottomPadding.value > 0) {
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.hazeEffect(
state = hazeState,
style = hazeStyle
)
.background(Color.Transparent)
.height(bottomPadding)
.fillMaxWidth()
)
}
}
}
}
}
}
@@ -13,99 +13,96 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.meloda.overseerr.screens.url.UrlViewModel
import dev.meloda.overseerr.screens.url.UrlViewModelImpl
import kotlinx.serialization.Serializable
import org.koin.compose.viewmodel.koinViewModel
class UrlScreen : Screen {
@Serializable
data object UrlScreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel: UrlViewModel = koinViewModel<UrlViewModelImpl>()
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
@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 = navigator::pop) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text(text = "Url") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = screenState.url,
onValueChange = viewModel::onUrlInputChanged,
placeholder = { Text(text = "Url") },
isError = screenState.isWrongUrlError,
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Uri
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
modifier = Modifier.fillMaxWidth(),
value = screenState.plexToken,
onValueChange = viewModel::onPlexTokenInputChanged,
placeholder = { Text(text = "Token") },
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go
)
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = screenState.url,
onValueChange = viewModel::onUrlInputChanged,
placeholder = { Text(text = "Url") },
isError = screenState.isWrongUrlError,
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Uri
)
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
modifier = Modifier.fillMaxWidth(),
value = screenState.plexToken,
onValueChange = viewModel::onPlexTokenInputChanged,
placeholder = { Text(text = "Token") },
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go
)
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
Button(
onClick = viewModel::onLoadButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Button(
onClick = viewModel::onLoadButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Load")
}
Text(text = "Load")
}
Button(
onClick = viewModel::onSaveButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Save")
}
Button(
onClick = viewModel::onSaveButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Save")
}
Button(
onClick = viewModel::onTestButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Test")
}
Button(
onClick = viewModel::onTestButtonClicked,
modifier = Modifier.weight(0.3f)
) {
Text(text = "Test")
}
}
}
}
}
}
@@ -6,6 +6,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.*
import androidx.navigation.NavController
import dev.meloda.overseerr.settings.model.ThemeMode
internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) }
@@ -36,3 +37,6 @@ internal fun AppTheme(
@Composable
internal expect fun SystemAppearance(isDark: Boolean)
@Composable
internal expect fun NavigationSettings(navController: NavController)