simple screen with lazylist, pull-to-refresh and blur with Haze
This commit is contained in:
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+143
@@ -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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-14
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user