simple screen with lazylist, pull-to-refresh and blur with Haze

This commit is contained in:
2024-08-05 04:56:19 +03:00
parent 2c91f6bb62
commit 3b65d44f2f
8 changed files with 252 additions and 25 deletions
+5 -4
View File
@@ -69,8 +69,9 @@ kotlin {
implementation(libs.multiplatformSettings) implementation(libs.multiplatformSettings)
implementation(libs.koin.core) implementation(libs.koin.core)
implementation(libs.koin.compose) implementation(libs.koin.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") implementation(libs.haze)
implementation(libs.haze.materials)
} }
commonTest.dependencies { commonTest.dependencies {
@@ -101,11 +102,11 @@ kotlin {
android { android {
namespace = "dev.meloda.overseerr" namespace = "dev.meloda.overseerr"
compileSdk = 34 compileSdk = 35
defaultConfig { defaultConfig {
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 35
applicationId = "dev.meloda.overseerr.androidApp" applicationId = "dev.meloda.overseerr.androidApp"
versionCode = 1 versionCode = 1
@@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.FadeTransition 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 dev.meloda.overseerr.theme.AppTheme
import org.koin.compose.KoinContext import org.koin.compose.KoinContext
@@ -14,7 +14,7 @@ import org.koin.compose.KoinContext
internal fun App() = KoinContext { internal fun App() = KoinContext {
AppTheme { AppTheme {
Surface(modifier = Modifier.fillMaxSize()) { Surface(modifier = Modifier.fillMaxSize()) {
Navigator(UrlScreen()) { navigator -> Navigator(MainScreen()) { navigator ->
FadeTransition(navigator) FadeTransition(navigator)
} }
} }
@@ -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")
}
}
}
}
}
@@ -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())
)
}
}
}
}
@@ -2,5 +2,10 @@ package dev.meloda.overseerr.screens.url
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
class UrlViewModel : ViewModel() { interface UrlViewModel {
}
class UrlViewModelImpl : ViewModel(), UrlViewModel {
} }
@@ -1,37 +1,54 @@
package dev.meloda.overseerr.screens.url.presentation package dev.meloda.overseerr.screens.url.presentation
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons
import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.Button import androidx.compose.material3.*
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.LayoutDirection
import androidx.lifecycle.viewmodel.compose.viewModel 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.presentation.LoginScreen import dev.meloda.overseerr.screens.login.presentation.LoginScreen
import dev.meloda.overseerr.screens.url.UrlViewModel import dev.meloda.overseerr.screens.url.UrlViewModel
import dev.meloda.overseerr.screens.url.UrlViewModelImpl
class UrlScreen : Screen { class UrlScreen : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val viewModel: UrlViewModel = viewModel { UrlViewModel() } val viewModel: UrlViewModel = viewModel { UrlViewModelImpl() }
Scaffold(modifier = Modifier.fillMaxSize()) { padding -> Scaffold(
Column( modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize() topBar = {
.padding(padding) 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") Text(text = "Input url screen")
Button( Button(
onClick = { onClick = { navigator.push(LoginScreen()) }
navigator.push(LoginScreen())
}
) { ) {
Text(text = "Next") Text(text = "Next")
} }
+1 -1
View File
@@ -15,7 +15,7 @@ fun main() = application {
Window( Window(
title = "Overseerr", title = "Overseerr",
state = rememberWindowState(width = 800.dp, height = 600.dp), state = rememberWindowState(width = 800.dp, height = 600.dp),
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication
) { ) {
window.minimumSize = Dimension(350, 600) window.minimumSize = Dimension(350, 600)
App() App()
+6 -3
View File
@@ -12,10 +12,13 @@ ktor = "3.0.0-beta-2"
kotlinx-serialization = "1.7.1" kotlinx-serialization = "1.7.1"
multiplatformSettings = "1.1.1" multiplatformSettings = "1.1.1"
koin = "4.0.0-RC1" koin = "4.0.0-RC1"
viewmodel-compose = "2.8.0"
haze = "0.7.3"
[libraries] [libraries]
androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 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-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" } 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" } 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-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-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" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" }
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" }
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
[plugins] [plugins]