diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 0371108..3b2b092 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -69,8 +69,9 @@ kotlin { implementation(libs.multiplatformSettings) implementation(libs.koin.core) implementation(libs.koin.compose) - - implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.haze) + implementation(libs.haze.materials) } commonTest.dependencies { @@ -101,11 +102,11 @@ kotlin { android { namespace = "dev.meloda.overseerr" - compileSdk = 34 + compileSdk = 35 defaultConfig { minSdk = 26 - targetSdk = 34 + targetSdk = 35 applicationId = "dev.meloda.overseerr.androidApp" versionCode = 1 diff --git a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt index e614d65..edf6a1f 100644 --- a/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/App.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.FadeTransition -import dev.meloda.overseerr.screens.url.presentation.UrlScreen +import dev.meloda.overseerr.screens.main.MainScreen import dev.meloda.overseerr.theme.AppTheme import org.koin.compose.KoinContext @@ -14,7 +14,7 @@ import org.koin.compose.KoinContext internal fun App() = KoinContext { AppTheme { Surface(modifier = Modifier.fillMaxSize()) { - Navigator(UrlScreen()) { navigator -> + Navigator(MainScreen()) { navigator -> FadeTransition(navigator) } } 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 new file mode 100644 index 0000000..c1f3b66 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/main/MainScreen.kt @@ -0,0 +1,58 @@ +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.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.meloda.overseerr.screens.login.presentation.LoginScreen +import dev.meloda.overseerr.screens.requests.RequestsScreen +import dev.meloda.overseerr.screens.url.presentation.UrlScreen + +class MainScreen : Screen { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + + Scaffold( + topBar = { + TopAppBar(title = { Text(text = "Main screen") }) + } + ) { padding -> + Row( + modifier = Modifier + .padding(padding) + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Button( + onClick = { navigator.push(UrlScreen()) }, + modifier = Modifier.weight(0.3f) + ) { + Text(text = "Url") + } + + Button( + onClick = { navigator.push(LoginScreen()) }, + modifier = Modifier.weight(0.3f) + ) { + Text(text = "Login") + } + + Button( + onClick = { navigator.push(RequestsScreen()) }, + modifier = Modifier.weight(0.3f) + ) { + Text(text = "Requests") + } + } + } + } +} 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 new file mode 100644 index 0000000..cb8fe28 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/requests/RequestsScreen.kt @@ -0,0 +1,143 @@ +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/url/UrlViewModel.kt b/composeApp/src/commonMain/kotlin/dev/meloda/overseerr/screens/url/UrlViewModel.kt index 8b163f6..f7ff1ed 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 @@ -2,5 +2,10 @@ package dev.meloda.overseerr.screens.url import androidx.lifecycle.ViewModel -class UrlViewModel : ViewModel() { +interface UrlViewModel { + +} + +class UrlViewModelImpl : ViewModel(), UrlViewModel { + } 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 485aac9..d2ef5e2 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 @@ -1,37 +1,54 @@ package dev.meloda.overseerr.screens.url.presentation -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.* +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.ui.Modifier +import androidx.compose.ui.unit.LayoutDirection 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.presentation.LoginScreen import dev.meloda.overseerr.screens.url.UrlViewModel +import dev.meloda.overseerr.screens.url.UrlViewModelImpl class UrlScreen : Screen { + @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val viewModel: UrlViewModel = viewModel { UrlViewModel() } + val viewModel: UrlViewModel = viewModel { UrlViewModelImpl() } - Scaffold(modifier = Modifier.fillMaxSize()) { padding -> - Column( - modifier = Modifier.fillMaxSize() - .padding(padding) + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { Text(text = "Url") }, + navigationIcon = { + IconButton(onClick = navigator::pop) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null + ) + } + } + ) + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) + .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) + .padding(bottom = padding.calculateBottomPadding()) ) { Text(text = "Input url screen") Button( - onClick = { - navigator.push(LoginScreen()) - } + onClick = { navigator.push(LoginScreen()) } ) { Text(text = "Next") } diff --git a/composeApp/src/jvmMain/kotlin/main.kt b/composeApp/src/jvmMain/kotlin/main.kt index c46f9b6..591f256 100644 --- a/composeApp/src/jvmMain/kotlin/main.kt +++ b/composeApp/src/jvmMain/kotlin/main.kt @@ -15,7 +15,7 @@ fun main() = application { Window( title = "Overseerr", state = rememberWindowState(width = 800.dp, height = 600.dp), - onCloseRequest = ::exitApplication, + onCloseRequest = ::exitApplication ) { window.minimumSize = Dimension(350, 600) App() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59aaf99..7587211 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,10 +12,13 @@ ktor = "3.0.0-beta-2" kotlinx-serialization = "1.7.1" multiplatformSettings = "1.1.1" koin = "4.0.0-RC1" +viewmodel-compose = "2.8.0" +haze = "0.7.3" [libraries] androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "viewmodel-compose" } androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" } androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } @@ -30,13 +33,13 @@ 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-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" } -ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } +haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } +haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } + [plugins]