simple ktor-client implementation
This commit is contained in:
@@ -84,14 +84,18 @@ kotlin {
|
|||||||
implementation(libs.coil.network.ktor)
|
implementation(libs.coil.network.ktor)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.ktor.core)
|
implementation(libs.ktor.core)
|
||||||
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
|
implementation(libs.ktor.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.viewmodel)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.haze)
|
implementation(libs.haze)
|
||||||
implementation(libs.haze.materials)
|
implementation(libs.haze.materials)
|
||||||
implementation(libs.kstore)
|
implementation(libs.kstore)
|
||||||
implementation(libs.napier)
|
implementation(libs.napier)
|
||||||
|
implementation(libs.message.bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
@@ -124,6 +128,7 @@ kotlin {
|
|||||||
|
|
||||||
wasmJsMain.dependencies {
|
wasmJsMain.dependencies {
|
||||||
implementation(libs.kstore.storage)
|
implementation(libs.kstore.storage)
|
||||||
|
implementation(libs.ktor.client.js)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -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
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package dev.meloda.overseerr.di
|
|||||||
import dev.meloda.overseerr.model.Platform
|
import dev.meloda.overseerr.model.Platform
|
||||||
import dev.meloda.overseerr.network.di.networkModule
|
import dev.meloda.overseerr.network.di.networkModule
|
||||||
import dev.meloda.overseerr.screens.login.di.loginModule
|
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.screens.url.di.urlModule
|
||||||
import dev.meloda.overseerr.settings.di.settingsModule
|
import dev.meloda.overseerr.settings.di.settingsModule
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
@@ -15,6 +16,7 @@ val appModule = module {
|
|||||||
settingsModule,
|
settingsModule,
|
||||||
networkModule,
|
networkModule,
|
||||||
loginModule,
|
loginModule,
|
||||||
urlModule
|
urlModule,
|
||||||
|
requestsModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package dev.meloda.overseerr.network.di
|
package dev.meloda.overseerr.network.di
|
||||||
|
|
||||||
|
import dev.meloda.overseerr.network.model.HttpClientEngineFactoryProvider
|
||||||
import io.ktor.client.*
|
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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val networkModule = module {
|
val networkModule = module {
|
||||||
|
singleOf(::HttpClientEngineFactoryProvider)
|
||||||
single {
|
single {
|
||||||
HttpClient {
|
HttpClient(engineFactory = get<HttpClientEngineFactoryProvider>().get()) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.meloda.overseerr.network.model
|
||||||
|
|
||||||
|
import io.ktor.client.engine.*
|
||||||
|
|
||||||
|
expect class HttpClientEngineFactoryProvider() {
|
||||||
|
fun get(): HttpClientEngineFactory<*>
|
||||||
|
}
|
||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
package dev.meloda.overseerr.screens.login.di
|
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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val loginModule = module {
|
val loginModule = module {
|
||||||
|
viewModelOf(::LoginViewModelImpl)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -17,13 +17,13 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.meloda.overseerr.screens.login.LoginViewModel
|
import dev.meloda.overseerr.screens.login.LoginViewModel
|
||||||
import dev.meloda.overseerr.screens.login.LoginViewModelImpl
|
import dev.meloda.overseerr.screens.login.LoginViewModelImpl
|
||||||
import dev.meloda.overseerr.screens.login.model.LoginScreenState
|
import dev.meloda.overseerr.screens.login.model.LoginScreenState
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
class LoginScreen : Screen {
|
class LoginScreen : Screen {
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class LoginScreen : Screen {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val viewModel: LoginViewModel = viewModel { LoginViewModelImpl() }
|
val viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||||
val screenState: LoginScreenState by viewModel.screenState.collectAsState()
|
val screenState: LoginScreenState by viewModel.screenState.collectAsState()
|
||||||
|
|
||||||
var loginValue by rememberSaveable {
|
var loginValue by rememberSaveable {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import cafe.adriel.voyager.core.screen.Screen
|
|||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.meloda.overseerr.screens.login.presentation.LoginScreen
|
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
|
import dev.meloda.overseerr.screens.url.presentation.UrlScreen
|
||||||
|
|
||||||
class MainScreen : Screen {
|
class MainScreen : Screen {
|
||||||
|
|||||||
-143
@@ -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())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+78
@@ -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<RequestsScreenState>
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
+9
@@ -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)
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package dev.meloda.overseerr.screens.requests.model
|
||||||
|
|
||||||
|
import dev.meloda.overseerr.screens.requests.ApiInfo
|
||||||
|
|
||||||
|
data class RequestsScreenState(
|
||||||
|
val dummyItems: List<Int>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+163
@@ -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<RequestsViewModelImpl>()
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ interface UrlViewModel {
|
|||||||
val screenState: StateFlow<UrlScreenState>
|
val screenState: StateFlow<UrlScreenState>
|
||||||
|
|
||||||
fun onUrlInputChanged(newText: String)
|
fun onUrlInputChanged(newText: String)
|
||||||
|
fun onPlexTokenInputChanged(newToken: String)
|
||||||
fun onLoadButtonClicked()
|
fun onLoadButtonClicked()
|
||||||
fun onSaveButtonClicked()
|
fun onSaveButtonClicked()
|
||||||
fun onTestButtonClicked()
|
fun onTestButtonClicked()
|
||||||
@@ -29,7 +30,14 @@ class UrlViewModelImpl(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
settingsController.settings
|
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)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,18 +45,30 @@ class UrlViewModelImpl(
|
|||||||
screenState.setValue { old -> old.copy(url = newText) }
|
screenState.setValue { old -> old.copy(url = newText) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPlexTokenInputChanged(newToken: String) {
|
||||||
|
screenState.setValue { old -> old.copy(plexToken = newToken) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLoadButtonClicked() {
|
override fun onLoadButtonClicked() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val settings = settingsController.loadAppSettings()
|
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() {
|
override fun onSaveButtonClicked() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsController.updateAppSettings { settings ->
|
settingsController.updateAppSettings { settings ->
|
||||||
settings.copy(url = screenState.value.url)
|
settings.copy(
|
||||||
|
url = screenState.value.url,
|
||||||
|
plexToken = screenState.value.plexToken
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package dev.meloda.overseerr.screens.url.di
|
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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val urlModule = module {
|
val urlModule = module {
|
||||||
|
viewModelOf(::UrlViewModelImpl)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -2,11 +2,13 @@ package dev.meloda.overseerr.screens.url.model
|
|||||||
|
|
||||||
data class UrlScreenState(
|
data class UrlScreenState(
|
||||||
val url: String,
|
val url: String,
|
||||||
|
val plexToken: String,
|
||||||
val isWrongUrlError: Boolean
|
val isWrongUrlError: Boolean
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY: UrlScreenState = UrlScreenState(
|
val EMPTY: UrlScreenState = UrlScreenState(
|
||||||
url = "",
|
url = "",
|
||||||
|
plexToken = "",
|
||||||
isWrongUrlError = false
|
isWrongUrlError = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-7
@@ -13,14 +13,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.meloda.overseerr.screens.url.UrlViewModel
|
import dev.meloda.overseerr.screens.url.UrlViewModel
|
||||||
import dev.meloda.overseerr.screens.url.UrlViewModelImpl
|
import dev.meloda.overseerr.screens.url.UrlViewModelImpl
|
||||||
import dev.meloda.overseerr.settings.SettingsController
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
|
||||||
|
|
||||||
class UrlScreen : Screen {
|
class UrlScreen : Screen {
|
||||||
|
|
||||||
@@ -28,10 +26,7 @@ class UrlScreen : Screen {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val viewModel: UrlViewModel = koinViewModel<UrlViewModelImpl>()
|
||||||
val settingsController: SettingsController = koinInject()
|
|
||||||
val viewModel: UrlViewModel = viewModel { UrlViewModelImpl(settingsController) }
|
|
||||||
|
|
||||||
val screenState by viewModel.screenState.collectAsState()
|
val screenState by viewModel.screenState.collectAsState()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -72,6 +67,19 @@ class UrlScreen : Screen {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AppSettings(
|
data class AppSettings(
|
||||||
val url: String
|
val url: String = "",
|
||||||
|
val plexToken: String = ""
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY: AppSettings = AppSettings(
|
val EMPTY: AppSettings = AppSettings()
|
||||||
url = ""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -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
|
||||||
|
}
|
||||||
+8
@@ -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
|
||||||
|
}
|
||||||
+8
@@ -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
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ haze = "0.7.3"
|
|||||||
kstore = "0.8.0"
|
kstore = "0.8.0"
|
||||||
appdirs = "1.2.2"
|
appdirs = "1.2.2"
|
||||||
napier = "2.7.1"
|
napier = "2.7.1"
|
||||||
|
message-bar = "1.0.5"
|
||||||
|
|
||||||
[libraries]
|
[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-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" }
|
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-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-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", 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" }
|
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-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||||
koin-compose = { module = "io.insert-koin:koin-compose", 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 = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||||
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
||||||
kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
|
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" }
|
kstore-storage = { module = "io.github.xxfast:kstore-storage", version.ref = "kstore" }
|
||||||
appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" }
|
appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" }
|
||||||
napier = { module = "io.github.aakira:napier", version.ref = "napier" }
|
napier = { module = "io.github.aakira:napier", version.ref = "napier" }
|
||||||
|
message-bar = { module = "com.stevdza-san:messagebarkmp", version.ref = "message-bar" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user