diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index eb3f740..b46ef8b 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -84,14 +84,18 @@ kotlin { implementation(libs.coil.network.ktor) implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.core) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json) implementation(libs.koin.core) implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.haze) implementation(libs.haze.materials) implementation(libs.kstore) implementation(libs.napier) + implementation(libs.message.bar) } commonTest.dependencies { @@ -124,6 +128,7 @@ kotlin { wasmJsMain.dependencies { implementation(libs.kstore.storage) + implementation(libs.ktor.client.js) } } } diff --git a/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.android.kt b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.android.kt new file mode 100644 index 0000000..b11a66c --- /dev/null +++ b/composeApp/src/androidMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.android.kt @@ -0,0 +1,8 @@ +package dev.meloda.overseerr.network.model + +import io.ktor.client.engine.* +import io.ktor.client.engine.okhttp.* + +actual class HttpClientEngineFactoryProvider actual constructor() { + actual fun get(): HttpClientEngineFactory<*> = OkHttp +} 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 fd46796..dca5ea1 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/di/AppModule.kt @@ -3,6 +3,7 @@ package dev.meloda.overseerr.di import dev.meloda.overseerr.model.Platform 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 @@ -15,6 +16,7 @@ val appModule = module { settingsModule, networkModule, loginModule, - urlModule + urlModule, + requestsModule ) } 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 cf6db0b..d9d4c92 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,12 +1,19 @@ package dev.meloda.overseerr.network.di +import dev.meloda.overseerr.network.model.HttpClientEngineFactoryProvider import io.ktor.client.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val networkModule = module { + singleOf(::HttpClientEngineFactoryProvider) single { - HttpClient { - + HttpClient(engineFactory = get().get()) { + install(ContentNegotiation) { + json() + } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.kt new file mode 100644 index 0000000..0c828d8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.kt @@ -0,0 +1,7 @@ +package dev.meloda.overseerr.network.model + +import io.ktor.client.engine.* + +expect class HttpClientEngineFactoryProvider() { + fun get(): HttpClientEngineFactory<*> +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/di/LoginModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/di/LoginModule.kt index 2892c3d..ce630d0 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/di/LoginModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/login/di/LoginModule.kt @@ -1,7 +1,9 @@ package dev.meloda.overseerr.screens.login.di +import dev.meloda.overseerr.screens.login.LoginViewModelImpl +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val loginModule = module { - + viewModelOf(::LoginViewModelImpl) } 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 4a40117..aae103f 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 @@ -17,13 +17,13 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel 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 org.koin.compose.viewmodel.koinViewModel class LoginScreen : Screen { @@ -31,7 +31,7 @@ class LoginScreen : Screen { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val viewModel: LoginViewModel = viewModel { LoginViewModelImpl() } + val viewModel: LoginViewModel = koinViewModel() val screenState: LoginScreenState by viewModel.screenState.collectAsState() var loginValue by rememberSaveable { 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 c1f3b66..b6e9c11 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 @@ -11,7 +11,7 @@ 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.presentation.LoginScreen -import dev.meloda.overseerr.screens.requests.RequestsScreen +import dev.meloda.overseerr.screens.requests.presentation.RequestsScreen import dev.meloda.overseerr.screens.url.presentation.UrlScreen class MainScreen : Screen { diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsScreen.kt deleted file mode 100644 index cb8fe28..0000000 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -package dev.meloda.overseerr.screens.requests - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack -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.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 cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.haze -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials -import kotlinx.coroutines.delay -import kotlin.time.Duration.Companion.seconds - -class RequestsScreen : Screen { - - @OptIn( - ExperimentalMaterial3Api::class, - ExperimentalHazeMaterialsApi::class - ) - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - - var isRefreshing by remember { - mutableStateOf(false) - } - - val hazeState = remember { HazeState() } - val hazeStyle = HazeMaterials.ultraThin() - - val refreshState = rememberPullToRefreshState() - - var isNeedToRefresh by remember { - mutableStateOf(false) - } - - LaunchedEffect(isNeedToRefresh) { - if (isNeedToRefresh) { - isRefreshing = true - delay(2.seconds) - isRefreshing = false - isNeedToRefresh = false - } - } - - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - title = { Text(text = "Requests") }, - navigationIcon = { - IconButton(onClick = navigator::pop) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = null - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - modifier = Modifier - .hazeChild( - state = hazeState, - style = hazeStyle - ).fillMaxWidth() - ) - } - ) { 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() - .haze( - state = hazeState, - style = hazeStyle - ) - .pullToRefresh( - isRefreshing = isRefreshing, - state = refreshState, - onRefresh = { isNeedToRefresh = true } - ) - ) { - item { - Spacer(modifier = Modifier.height(padding.calculateTopPadding())) - } - items(count = 1000) { 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)) - } - } - - if (bottomPadding.value > 0) { - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .hazeChild( - state = hazeState, - style = hazeStyle - ) - .background(Color.Transparent) - .height(bottomPadding) - .fillMaxWidth() - ) - } - - Indicator( - state = refreshState, - isRefreshing = isRefreshing, - modifier = Modifier.align(Alignment.TopCenter) - .padding(top = padding.calculateTopPadding()) - ) - } - } - } -} 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 new file mode 100644 index 0000000..65f1ed2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsViewModel.kt @@ -0,0 +1,78 @@ +package dev.meloda.overseerr.screens.requests + +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 io.github.aakira.napier.Napier +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlin.time.Duration.Companion.seconds + +interface RequestsViewModel { + val screenState: StateFlow + + fun onRefresh() + + fun onSuccessMessageShown() + fun onErrorMessageShown() +} + +class RequestsViewModelImpl( + private val httpClient: HttpClient, + private val settingsController: SettingsController +) : ViewModel(), RequestsViewModel { + + override val screenState = MutableStateFlow(RequestsScreenState.EMPTY) + + override fun onRefresh() { + viewModelScope.launch { + screenState.setValue { old -> old.copy(isLoading = true) } + delay(1.seconds) + loadInfo() + screenState.setValue { old -> old.copy(isLoading = false) } + } + } + + override fun onSuccessMessageShown() { + screenState.setValue { old -> old.copy(apiInfo = null) } + } + + override fun onErrorMessageShown() { + screenState.setValue { old -> old.copy(apiErrorText = null) } + } + + private fun loadInfo() { + viewModelScope.launch { + kotlin.runCatching { + httpClient.get("${settingsController.settings.value.url}/api/v1") { + headers { + append("X-Api-Key", settingsController.settings.value.plexToken) + } + }.body() as ApiInfo + }.fold( + onSuccess = { response -> + Napier.d { "Response: $response" } + screenState.setValue { old -> old.copy(apiInfo = response) } + }, + onFailure = { error -> + Napier.e(error) { "Error occurred" } + screenState.setValue { old -> old.copy(apiErrorText = error.message.toString()) } + } + ) + } + } +} + +@Serializable +data class ApiInfo( + val api: String, + val version: String +) diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/di/RequestsModule.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/di/RequestsModule.kt new file mode 100644 index 0000000..1749dcf --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/di/RequestsModule.kt @@ -0,0 +1,9 @@ +package dev.meloda.overseerr.screens.requests.di + +import dev.meloda.overseerr.screens.requests.RequestsViewModelImpl +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val requestsModule = module { + viewModelOf(::RequestsViewModelImpl) +} diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/model/RequestsScreenState.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/model/RequestsScreenState.kt new file mode 100644 index 0000000..040acc3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/model/RequestsScreenState.kt @@ -0,0 +1,19 @@ +package dev.meloda.overseerr.screens.requests.model + +import dev.meloda.overseerr.screens.requests.ApiInfo + +data class RequestsScreenState( + val dummyItems: List, + val isLoading: Boolean, + val apiInfo: ApiInfo?, + val apiErrorText: String? +) { + companion object { + val EMPTY: RequestsScreenState = RequestsScreenState( + dummyItems = List(50) { it }, + isLoading = false, + apiInfo = null, + apiErrorText = null + ) + } +} 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 new file mode 100644 index 0000000..a66ffbb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/presentation/RequestsScreen.kt @@ -0,0 +1,163 @@ +package dev.meloda.overseerr.screens.requests.presentation + +import ContentWithMessageBar +import MessageBarPosition +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.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.* +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 cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild +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 org.koin.compose.viewmodel.koinViewModel +import rememberMessageBarState + +class RequestsScreen : Screen { + + @OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class + ) + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val viewModel: RequestsViewModel = koinViewModel() + val screenState: RequestsScreenState by viewModel.screenState.collectAsState() + + val hazeState = remember { HazeState() } + val hazeStyle = HazeMaterials.ultraThin() + + val refreshState = rememberPullToRefreshState() + val messageBarState = rememberMessageBarState() + + LaunchedEffect(screenState) { + if (screenState.apiErrorText != null) { + messageBarState.addError(Exception(screenState.apiErrorText)) + viewModel.onErrorMessageShown() + } + + if (screenState.apiInfo != null) { + messageBarState.addSuccess(screenState.apiInfo.toString()) + 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 + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + modifier = Modifier + .hazeChild( + state = hazeState, + style = hazeStyle + ).fillMaxWidth(), + actions = { + IconButton( + onClick = viewModel::onRefresh + ) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + } + } + ) + } + ) { padding -> + val bottomPadding = padding.calculateBottomPadding() + + ContentWithMessageBar( + messageBarState = messageBarState, + position = MessageBarPosition.BOTTOM + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) + .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .haze( + state = hazeState, + style = hazeStyle + ) + .pullToRefresh( + isRefreshing = screenState.isLoading, + state = refreshState, + onRefresh = viewModel::onRefresh + ) + ) { + item { + Spacer(modifier = Modifier.height(padding.calculateTopPadding())) + } + 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) + .hazeChild( + state = hazeState, + style = hazeStyle + ) + .background(Color.Transparent) + .height(bottomPadding) + .fillMaxWidth() + ) + } + } + } + } + } +} 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 index dae7cf4..d0bc9e1 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/UrlViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/UrlViewModel.kt @@ -16,6 +16,7 @@ interface UrlViewModel { val screenState: StateFlow fun onUrlInputChanged(newText: String) + fun onPlexTokenInputChanged(newToken: String) fun onLoadButtonClicked() fun onSaveButtonClicked() fun onTestButtonClicked() @@ -29,7 +30,14 @@ class UrlViewModelImpl( init { settingsController.settings - .onEach { settings -> screenState.setValue { old -> old.copy(url = settings.url) } } + .onEach { settings -> + screenState.setValue { old -> + old.copy( + url = settings.url, + plexToken = settings.plexToken + ) + } + } .launchIn(viewModelScope) } @@ -37,18 +45,30 @@ class UrlViewModelImpl( 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) } + 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) + settings.copy( + url = screenState.value.url, + plexToken = screenState.value.plexToken + ) } } } 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 index ea56935..9ec4824 100644 --- 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 @@ -1,7 +1,9 @@ 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/model/UrlScreenState.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/model/UrlScreenState.kt index 33d685a..b74ed12 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/model/UrlScreenState.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/model/UrlScreenState.kt @@ -2,11 +2,13 @@ package dev.meloda.overseerr.screens.url.model data class UrlScreenState( val url: String, + val plexToken: String, val isWrongUrlError: Boolean ) { companion object { val EMPTY: UrlScreenState = UrlScreenState( url = "", + plexToken = "", isWrongUrlError = false ) } diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt index 9d737b1..3ba946c 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/presentation/UrlScreen.kt @@ -13,14 +13,12 @@ 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.viewmodel.compose.viewModel 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 dev.meloda.overseerr.settings.SettingsController -import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel class UrlScreen : Screen { @@ -28,10 +26,7 @@ class UrlScreen : Screen { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - - val settingsController: SettingsController = koinInject() - val viewModel: UrlViewModel = viewModel { UrlViewModelImpl(settingsController) } - + val viewModel: UrlViewModel = koinViewModel() val screenState by viewModel.screenState.collectAsState() Scaffold( @@ -72,6 +67,19 @@ class UrlScreen : Screen { 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) diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt index c564c49..a08951a 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/settings/model/AppSettings.kt @@ -4,11 +4,10 @@ import kotlinx.serialization.Serializable @Serializable data class AppSettings( - val url: String + val url: String = "", + val plexToken: String = "" ) { companion object { - val EMPTY: AppSettings = AppSettings( - url = "" - ) + val EMPTY: AppSettings = AppSettings() } } diff --git a/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.ios.kt b/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.ios.kt new file mode 100644 index 0000000..f683a69 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.ios.kt @@ -0,0 +1,8 @@ +package dev.meloda.overseerr.network.model + +import io.ktor.client.engine.* +import io.ktor.client.engine.darwin.* + +actual class HttpClientEngineFactoryProvider actual constructor() { + actual fun get(): HttpClientEngineFactory<*> = Darwin +} diff --git a/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.jvm.kt new file mode 100644 index 0000000..b11a66c --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/dev/meloda/overseerr/network/model/HttpClientProvider.jvm.kt @@ -0,0 +1,8 @@ +package dev.meloda.overseerr.network.model + +import io.ktor.client.engine.* +import io.ktor.client.engine.okhttp.* + +actual class HttpClientEngineFactoryProvider actual constructor() { + actual fun get(): HttpClientEngineFactory<*> = OkHttp +} diff --git a/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.wasmJs.kt new file mode 100644 index 0000000..992d876 --- /dev/null +++ b/composeApp/src/wasmJsMain/kotlin/dev/meloda/overseerr/network/model/HttpClientEngineFactoryProvider.wasmJs.kt @@ -0,0 +1,8 @@ +package dev.meloda.overseerr.network.model + +import io.ktor.client.engine.* +import io.ktor.client.engine.js.* + +actual class HttpClientEngineFactoryProvider actual constructor() { + actual fun get(): HttpClientEngineFactory<*> = Js +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e88ea2..643364d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ haze = "0.7.3" kstore = "0.8.0" appdirs = "1.2.2" napier = "2.7.1" +message-bar = "1.0.5" [libraries] @@ -32,12 +33,15 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } -ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-kotlinx-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" } @@ -45,6 +49,7 @@ kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" kstore-storage = { module = "io.github.xxfast:kstore-storage", version.ref = "kstore" } appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" } napier = { module = "io.github.aakira:napier", version.ref = "napier" } +message-bar = { module = "com.stevdza-san:messagebarkmp", version.ref = "message-bar" } [plugins]