create new common components

This commit is contained in:
2026-08-07 06:09:28 +03:00
parent 7cc21d101a
commit b851cf5d6d
26 changed files with 221 additions and 228 deletions
@@ -1,20 +0,0 @@
package ru.shadowsparky.videobox.multiplatform.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import ru.shadowsparky.vbox.shared.presentation.list.ErrorAndRetry
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@Preview
@Composable
fun ErrorPreview() {
ErrorAndRetry(
errorMsg = "Неизвестная ошибка"
) {}
}
@Preview
@Composable
fun LoadingPreview() {
Loading()
}
@@ -5,7 +5,6 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@@ -14,6 +13,7 @@ import androidx.compose.ui.Modifier
import com.seiko.imageloader.LocalImageLoader import com.seiko.imageloader.LocalImageLoader
import ru.shadowsparky.koin import ru.shadowsparky.koin
import ru.shadowsparky.ui.components.BackButton import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.ui.nav.Nav import ru.shadowsparky.ui.nav.Nav
import ru.shadowsparky.updater.presentation.UpdateDialog import ru.shadowsparky.updater.presentation.UpdateDialog
import ru.shadowsparky.vbox.shared.presentation.list.VideoScreen import ru.shadowsparky.vbox.shared.presentation.list.VideoScreen
@@ -3,17 +3,12 @@ package ru.shadowsparky.vbox.shared.presentation
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import ru.shadowsparky.ui.AppScaffold import ru.shadowsparky.ui.AppScaffold
import ru.shadowsparky.ui.LocalBlurState import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.ui.rememberBlurState import ru.shadowsparky.ui.rememberBlurState
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.presentation.chat.ChatScreen import ru.shadowsparky.vbox.shared.presentation.chat.ChatScreen
import ru.shadowsparky.vbox.shared.presentation.list.VideoContent import ru.shadowsparky.vbox.shared.presentation.list.VideoContent
import ru.shadowsparky.vbox.shared.presentation.nav.Child import ru.shadowsparky.vbox.shared.presentation.nav.Child
@@ -40,7 +35,6 @@ fun MainScreen(route: Route, child: Child, root: VBoxRootComponent) {
fun BaseMainScreen( fun BaseMainScreen(
appBar: @Composable () -> Unit, appBar: @Composable () -> Unit,
route: Route, route: Route,
scrollBehavior: TopAppBarScrollBehavior,
onNavigate: (Route) -> Unit, onNavigate: (Route) -> Unit,
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit
) { ) {
@@ -51,8 +45,7 @@ fun BaseMainScreen(
currentRoute = route, currentRoute = route,
onSelectNavigation = { onNavigate(it) }, onSelectNavigation = { onNavigate(it) },
navigationEntries = createNavigationList(), navigationEntries = createNavigationList(),
colorScheme = getColorScheme(), colorScheme = getColorScheme()
scrollBehavior = scrollBehavior
) { padding -> ) { padding ->
content(padding) content(padding)
} }
@@ -63,20 +56,15 @@ fun BaseMainScreen(
@Composable @Composable
private fun MainScreenInternal(route: Route, child: Child, onNavigate: (Route) -> Unit) { private fun MainScreenInternal(route: Route, child: Child, onNavigate: (Route) -> Unit) {
val navList = createNavigationList() val navList = createNavigationList()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
BaseMainScreen( BaseMainScreen(
appBar = { appBar = {
navList.findTitle(route)?.let { title -> navList.findTitle(route)?.let { title ->
TopAppBar( TopAppBar(
title = { Text(title) }, title = { Text(title) }
scrollBehavior = scrollBehavior,
modifier = Modifier.applyBlurEffect(LocalBlurState.current),
colors = ComponentDefaults.topAppBarColors()
) )
} }
}, },
route = route, route = route,
scrollBehavior = scrollBehavior,
onNavigate = onNavigate, onNavigate = onNavigate,
content = { padding -> content = { padding ->
when (child) { when (child) {
@@ -4,18 +4,13 @@ import androiddev.apps.vbox.client.shared.generated.resources.Res
import androiddev.apps.vbox.client.shared.generated.resources.chat_title_app_bar import androiddev.apps.vbox.client.shared.generated.resources.chat_title_app_bar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.extensions.compose.subscribeAsState
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.chat.presentation.ChatComponent import ru.shadowsparky.chat.presentation.ChatComponent
import ru.shadowsparky.chat.presentation.ChatContent import ru.shadowsparky.chat.presentation.ChatContent
import ru.shadowsparky.ui.LocalBlurState import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen
import ru.shadowsparky.vbox.shared.presentation.nav.Route import ru.shadowsparky.vbox.shared.presentation.nav.Route
@@ -25,15 +20,10 @@ fun ChatScreen(component: ChatComponent, route: Route, onNavigate: (Route) -> Un
val state by component.state.subscribeAsState() val state by component.state.subscribeAsState()
BaseMainScreen( BaseMainScreen(
appBar = { appBar = {
TopAppBar( TopAppBar(title = { Text(stringResource(Res.string.chat_title_app_bar)) })
title = { Text(stringResource(Res.string.chat_title_app_bar)) },
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(LocalBlurState.current)
)
}, },
route, route,
onNavigate = onNavigate, onNavigate = onNavigate,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
content = { ChatContent(component, it, state) } content = { ChatContent(component, it, state) }
) )
} }
@@ -17,7 +17,6 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -26,11 +25,9 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -57,19 +54,17 @@ import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.LocalBlurState import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurSource import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.LazyVerticalGrid import ru.shadowsparky.ui.components.LazyVerticalGrid
import ru.shadowsparky.ui.components.Loading import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.rememberBlurState import ru.shadowsparky.ui.rememberBlurState
import ru.shadowsparky.vbox.shared.domain.model.VideoItem import ru.shadowsparky.vbox.shared.domain.model.VideoItem
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.search.AppBarWithSearch import ru.shadowsparky.vbox.shared.presentation.search.AppBarWithSearch
import ru.shadowsparky.vbox.shared.presentation.state.rememberTopAppBarStateByHolder
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun VideoScreen(component: VideoScreenComponent) { fun VideoScreen(component: VideoScreenComponent) {
val appBarState = rememberTopAppBarStateByHolder(component.topAppBarStateHolder)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(appBarState)
val error by component.errorCode.collectAsState(null) val error by component.errorCode.collectAsState(null)
val items by component.items.collectAsState() val items by component.items.collectAsState()
val blurState = rememberBlurState() val blurState = rememberBlurState()
@@ -93,8 +88,7 @@ fun VideoScreen(component: VideoScreenComponent) {
isDefault = title == appName, isDefault = title == appName,
onBack = { component.doBack() } onBack = { component.doBack() }
) )
}, }
scrollBehavior = scrollBehavior
) )
} }
} }
@@ -123,9 +117,10 @@ fun VideoScreenContent(
onShowOverview: (Long) -> Unit onShowOverview: (Long) -> Unit
) { ) {
if (error != null) { if (error != null) {
ErrorAndRetry(error) { ErrorContent(
onRefresh() error,
} buttonText = stringResource(Res.string.retry)
) { onRefresh() }
} else if (items != null) { } else if (items != null) {
AnimatedContent( AnimatedContent(
items, items,
@@ -259,29 +254,6 @@ fun Poster(
} }
} }
@Composable
fun ErrorAndRetry(errorMsg: String, retry: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(
text = errorMsg,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 4.dp),
textAlign = TextAlign.Center
)
Button(onClick = { retry() }) {
Text(
text = stringResource(Res.string.retry),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable @Composable
fun VideosNotFound(emptyResource: StringResource) { fun VideosNotFound(emptyResource: StringResource) {
Box( Box(
@@ -296,8 +268,3 @@ fun VideosNotFound(emptyResource: StringResource) {
) )
} }
} }
@Composable
fun Loading() {
Loading(Modifier.fillMaxSize())
}
@@ -19,13 +19,11 @@ import ru.shadowsparky.vbox.shared.presentation.createExceptionHandler
import ru.shadowsparky.vbox.shared.presentation.nav.Child import ru.shadowsparky.vbox.shared.presentation.nav.Child
import ru.shadowsparky.vbox.shared.presentation.nav.Route import ru.shadowsparky.vbox.shared.presentation.nav.Route
import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
import ru.shadowsparky.vbox.shared.presentation.state.TopAppBarStateHolder
class VideoScreenComponent( class VideoScreenComponent(
private val context: ComponentContext, private val context: ComponentContext,
private val videoApi: VideoApi, private val videoApi: VideoApi,
private val dispatcherProvider: DispatcherProvider, private val dispatcherProvider: DispatcherProvider,
val topAppBarStateHolder: TopAppBarStateHolder,
val search: String? = null, val search: String? = null,
val showOverview: (Long) -> Unit, val showOverview: (Long) -> Unit,
val doBack: () -> Unit val doBack: () -> Unit
@@ -53,7 +51,6 @@ class VideoScreenComponent(
@Factory @Factory
@Named(RouteIds.VIDEOS) @Named(RouteIds.VIDEOS)
class VideosChildFactory( class VideosChildFactory(
private val topAppBarStateHolder: TopAppBarStateHolder,
private val videoApi: VideoApi, private val videoApi: VideoApi,
private val dispatcherProvider: DispatcherProvider private val dispatcherProvider: DispatcherProvider
) : ComponentFactory<Child.VideosChild, Route.Videos, Route> { ) : ComponentFactory<Child.VideosChild, Route.Videos, Route> {
@@ -66,7 +63,6 @@ class VideosChildFactory(
child, child,
videoApi, videoApi,
dispatcherProvider, dispatcherProvider,
topAppBarStateHolder,
route.request, route.request,
showOverview = { root.nav(Route.Overview(it)) }, showOverview = { root.nav(Route.Overview(it)) },
doBack = { root.onBack() } doBack = { root.onBack() }
@@ -5,8 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -8,6 +8,7 @@ import androiddev.apps.vbox.client.shared.generated.resources.details_serial
import androiddev.apps.vbox.client.shared.generated.resources.more import androiddev.apps.vbox.client.shared.generated.resources.more
import androiddev.apps.vbox.client.shared.generated.resources.offline_remove import androiddev.apps.vbox.client.shared.generated.resources.offline_remove
import androiddev.apps.vbox.client.shared.generated.resources.offline_save import androiddev.apps.vbox.client.shared.generated.resources.offline_save
import androiddev.apps.vbox.client.shared.generated.resources.retry
import androiddev.apps.vbox.client.shared.generated.resources.share import androiddev.apps.vbox.client.shared.generated.resources.share
import androiddev.apps.vbox.client.shared.generated.resources.star import androiddev.apps.vbox.client.shared.generated.resources.star
import androiddev.apps.vbox.client.shared.generated.resources.star_unchecked import androiddev.apps.vbox.client.shared.generated.resources.star_unchecked
@@ -41,11 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -72,26 +69,23 @@ import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.domain.Clip import ru.shadowsparky.domain.Clip
import ru.shadowsparky.ui.LocalBlurState import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.applyBlurSource import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.BackButton import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.LazyColumn import ru.shadowsparky.ui.components.LazyColumn
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.ui.rememberBlurState import ru.shadowsparky.ui.rememberBlurState
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.getClipboardText import ru.shadowsparky.vbox.shared.presentation.getClipboardText
import ru.shadowsparky.vbox.shared.presentation.list.ErrorAndRetry
import ru.shadowsparky.vbox.shared.presentation.list.Loading
import ru.shadowsparky.vbox.shared.presentation.settings.tag.LinkTagBottomSheet import ru.shadowsparky.vbox.shared.presentation.settings.tag.LinkTagBottomSheet
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun OverviewScreen(component: OverviewComponent) { fun OverviewScreen(component: OverviewComponent) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val errorState by component.errorCode.collectAsState(initial = null) val errorState by component.errorCode.collectAsState(initial = null)
val details by component.details.collectAsState() val details by component.details.collectAsState()
val snackbarState = remember { SnackbarHostState() }
val blurState = rememberBlurState() val blurState = rememberBlurState()
var tagsBottomSheetVisible by rememberSaveable { mutableStateOf(false) } var tagsBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
CompositionLocalProvider(LocalBlurState provides blurState) { CompositionLocalProvider(LocalBlurState provides blurState) {
@@ -101,17 +95,15 @@ fun OverviewScreen(component: OverviewComponent) {
component.offlineSupport, component.offlineSupport,
details, details,
component, component,
scrollBehavior,
tagsRequest = { tagsBottomSheetVisible = true } tagsRequest = { tagsBottomSheetVisible = true }
) )
}, },
scrollBehavior = scrollBehavior,
snackbarState = snackbarState,
content = { padding -> content = { padding ->
when { when {
errorState != null -> ErrorAndRetry( errorState != null -> ErrorContent(
errorMsg = errorState!! errorState!!,
buttonText = stringResource(Res.string.retry)
) { component.loadDetailsIfNeeded() } ) { component.loadDetailsIfNeeded() }
details != null -> DetailsInfo( details != null -> DetailsInfo(
@@ -145,12 +137,10 @@ private fun OverviewAppBar(
offlineSupport: Boolean, offlineSupport: Boolean,
details: VideoDetails?, details: VideoDetails?,
component: OverviewComponent, component: OverviewComponent,
scrollBehavior: TopAppBarScrollBehavior,
tagsRequest: () -> Unit tagsRequest: () -> Unit
) { ) {
val isSaved by component.isSaved.collectAsStateWithLifecycle(null) val isSaved by component.isSaved.collectAsStateWithLifecycle(null)
val offlineItem by component.offlineItem.collectAsStateWithLifecycle(null) val offlineItem by component.offlineItem.collectAsStateWithLifecycle(null)
val blur = LocalBlurState.current
TopAppBar( TopAppBar(
title = { title = {
details?.title?.let { details?.title?.let {
@@ -162,7 +152,6 @@ private fun OverviewAppBar(
} }
}, },
navigationIcon = { BackButton { component.doBack() } }, navigationIcon = { BackButton { component.doBack() } },
colors = ComponentDefaults.topAppBarColors(),
actions = { actions = {
details?.let { details?.let {
if (component.shareManager.canShare()) { if (component.shareManager.canShare()) {
@@ -192,9 +181,7 @@ private fun OverviewAppBar(
tagsRequest tagsRequest
) { component.updateOffline(offlineItem) } ) { component.updateOffline(offlineItem) }
} }
}, }
scrollBehavior = scrollBehavior,
modifier = Modifier.applyBlurEffect(blur)
) )
} }
@@ -7,6 +7,7 @@ import androiddev.apps.vbox.client.shared.generated.resources.episodes_overview
import androiddev.apps.vbox.client.shared.generated.resources.eye import androiddev.apps.vbox.client.shared.generated.resources.eye
import androiddev.apps.vbox.client.shared.generated.resources.folder_check import androiddev.apps.vbox.client.shared.generated.resources.folder_check
import androiddev.apps.vbox.client.shared.generated.resources.pending import androiddev.apps.vbox.client.shared.generated.resources.pending
import androiddev.apps.vbox.client.shared.generated.resources.retry
import androiddev.apps.vbox.client.shared.generated.resources.save_as import androiddev.apps.vbox.client.shared.generated.resources.save_as
import androiddev.apps.vbox.client.shared.generated.resources.select_all import androiddev.apps.vbox.client.shared.generated.resources.select_all
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@@ -26,8 +27,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -47,19 +46,18 @@ import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.domain.Clip import ru.shadowsparky.domain.Clip
import ru.shadowsparky.ui.LocalBlurState import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.applyBlurSource import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.BackButton import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.ExpressiveLazyColumn import ru.shadowsparky.ui.components.ExpressiveLazyColumn
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.ui.rememberBlurState import ru.shadowsparky.ui.rememberBlurState
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.domain.OfflineState import ru.shadowsparky.vbox.shared.domain.OfflineState
import ru.shadowsparky.vbox.shared.domain.OfflineVideo import ru.shadowsparky.vbox.shared.domain.OfflineVideo
import ru.shadowsparky.vbox.shared.domain.model.Episode import ru.shadowsparky.vbox.shared.domain.model.Episode
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.getClipboardText import ru.shadowsparky.vbox.shared.presentation.getClipboardText
import ru.shadowsparky.vbox.shared.presentation.list.ErrorAndRetry
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -96,7 +94,6 @@ fun EpisodesOverviewContent(
startOffline: () -> Unit, startOffline: () -> Unit,
commitOffline: (Boolean) -> Unit commitOffline: (Boolean) -> Unit
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
val blurState = rememberBlurState() val blurState = rememberBlurState()
var isSaveMode by rememberSaveable { mutableStateOf(false) } var isSaveMode by rememberSaveable { mutableStateOf(false) }
@@ -133,9 +130,6 @@ fun EpisodesOverviewContent(
} }
} }
}, },
scrollBehavior = scrollBehavior,
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(blurState),
actions = { actions = {
if (offlineSupport) { if (offlineSupport) {
IconButton( IconButton(
@@ -165,8 +159,7 @@ fun EpisodesOverviewContent(
} }
} }
) )
}, }
scrollBehavior = scrollBehavior
) )
} }
@@ -184,7 +177,10 @@ fun EpisodesOverviewList(
onOfflineClicked: (EpisodeWithOfflineInfo) -> Unit onOfflineClicked: (EpisodeWithOfflineInfo) -> Unit
) { ) {
if (errorCode != null) { if (errorCode != null) {
ErrorAndRetry(errorMsg = errorCode) { loadLinks() } ErrorContent(
errorCode,
buttonText = stringResource(Res.string.retry)
) { loadLinks() }
} else if (episodes != null) { } else if (episodes != null) {
val blur = LocalBlurState.current val blur = LocalBlurState.current
val requester = remember { FocusRequester() } val requester = remember { FocusRequester() }
@@ -34,6 +34,8 @@ class SeasonOverviewComponent(
val error = _error.asSharedFlow() val error = _error.asSharedFlow()
private val exceptionHandler = createExceptionHandler(componentScope) { _error.emit(it) } private val exceptionHandler = createExceptionHandler(componentScope) { _error.emit(it) }
init { load() }
fun load() { fun load() {
componentScope.launch(dispatchers.main + exceptionHandler) { componentScope.launch(dispatchers.main + exceptionHandler) {
_error.emit(null) _error.emit(null)
@@ -1,6 +1,7 @@
package ru.shadowsparky.vbox.shared.presentation.overview.season package ru.shadowsparky.vbox.shared.presentation.overview.season
import androiddev.apps.vbox.client.shared.generated.resources.Res import androiddev.apps.vbox.client.shared.generated.resources.Res
import androiddev.apps.vbox.client.shared.generated.resources.retry
import androiddev.apps.vbox.client.shared.generated.resources.seasons_overview import androiddev.apps.vbox.client.shared.generated.resources.seasons_overview
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -8,8 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -22,22 +21,19 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.LocalBlurState import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.applyBlurSource import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.BackButton import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.ExpressiveLazyColumn import ru.shadowsparky.ui.components.ExpressiveLazyColumn
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.ui.rememberBlurState import ru.shadowsparky.ui.rememberBlurState
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.domain.model.Season import ru.shadowsparky.vbox.shared.domain.model.Season
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.list.ErrorAndRetry
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SeasonOverviewScreen(component: SeasonOverviewComponent) { fun SeasonOverviewScreen(component: SeasonOverviewComponent) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
LaunchedEffect(null) { component.load() }
val blurState = rememberBlurState() val blurState = rememberBlurState()
CompositionLocalProvider(LocalBlurState provides blurState) { CompositionLocalProvider(LocalBlurState provides blurState) {
BaseScreen( BaseScreen(
@@ -45,9 +41,10 @@ fun SeasonOverviewScreen(component: SeasonOverviewComponent) {
val seasons = component.season.collectAsState(null) val seasons = component.season.collectAsState(null)
val err = component.error.collectAsState(null) val err = component.error.collectAsState(null)
if (err.value != null) { if (err.value != null) {
ErrorAndRetry(err.value!!) { ErrorContent(
component.load() err.value!!,
} buttonText = stringResource(Res.string.retry)
) { component.load() }
} else if (seasons.value != null) { } else if (seasons.value != null) {
SeasonsInfo( SeasonsInfo(
seasons.value!!, it seasons.value!!, it
@@ -61,11 +58,9 @@ fun SeasonOverviewScreen(component: SeasonOverviewComponent) {
Text(stringResource(Res.string.seasons_overview)) Text(stringResource(Res.string.seasons_overview))
}, navigationIcon = { }, navigationIcon = {
BackButton { component.doBack() } BackButton { component.doBack() }
}, scrollBehavior = scrollBehavior, }
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(blurState)
) )
}, scrollBehavior = scrollBehavior }
) )
} }
} }
@@ -2,6 +2,7 @@ package ru.shadowsparky.vbox.shared.presentation.player
import androiddev.apps.vbox.client.shared.generated.resources.Res import androiddev.apps.vbox.client.shared.generated.resources.Res
import androiddev.apps.vbox.client.shared.generated.resources.next_episode import androiddev.apps.vbox.client.shared.generated.resources.next_episode
import androiddev.apps.vbox.client.shared.generated.resources.retry
import androiddev.apps.vbox.client.shared.generated.resources.start_from import androiddev.apps.vbox.client.shared.generated.resources.start_from
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -9,7 +10,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -28,8 +28,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.AppScaffold import ru.shadowsparky.ui.AppScaffold
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.Loading import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.vbox.shared.presentation.list.ErrorAndRetry
import ru.shadowsparky.videoplayer.PlayerEvent import ru.shadowsparky.videoplayer.PlayerEvent
import ru.shadowsparky.videoplayer.ui.VideoPlayer import ru.shadowsparky.videoplayer.ui.VideoPlayer
@@ -47,12 +47,11 @@ fun PlayerScreen(component: PlayerComponent) {
when (val state = e.state) { when (val state = e.state) {
is PlayerEvent.Idle -> Loading(Modifier.fillMaxSize()) is PlayerEvent.Idle -> Loading(Modifier.fillMaxSize())
is PlayerEvent.Error -> { is PlayerEvent.Error -> {
Column(modifier = Modifier.fillMaxSize().padding(it)) { ErrorContent(
ErrorAndRetry( state.message,
state.message, modifier = Modifier.padding(it),
retry = { component.load() } buttonText = stringResource(Res.string.retry),
) ) { component.load() }
}
} }
is PlayerEvent.Ended -> component.requestNextEpisode() is PlayerEvent.Ended -> component.requestNextEpisode()
@@ -18,8 +18,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -31,11 +29,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.components.AlertContent import ru.shadowsparky.ui.components.AlertContent
import ru.shadowsparky.ui.components.LazyColumn import ru.shadowsparky.ui.components.LazyColumn
import ru.shadowsparky.ui.theme.ComponentDefaults import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.vbox.shared.domain.tag.TagInfo import ru.shadowsparky.vbox.shared.domain.tag.TagInfo
import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen
import ru.shadowsparky.vbox.shared.presentation.list.VideoScreenContent import ru.shadowsparky.vbox.shared.presentation.list.VideoScreenContent
@@ -59,13 +55,10 @@ fun SavedMovieScreen(comp: SavedMovieComponent, route: Route, onNavigate: (Route
contentDescription = null contentDescription = null
) )
} }
}, }
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(LocalBlurState.current)
) )
}, },
route = route, route = route,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
onNavigate = onNavigate, onNavigate = onNavigate,
content = { content = {
if (filterBottomSheetVisible) { if (filterBottomSheetVisible) {
@@ -103,7 +103,6 @@ fun SearchScreen(
) { textFieldValue = it } ) { textFieldValue = it }
}, },
route = route, route = route,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
onNavigate = onNavigate onNavigate = onNavigate
) )
} }
@@ -7,17 +7,13 @@ import androiddev.apps.vbox.client.shared.generated.resources.account_password
import androiddev.apps.vbox.client.shared.generated.resources.account_reg_hint import androiddev.apps.vbox.client.shared.generated.resources.account_reg_hint
import androiddev.apps.vbox.client.shared.generated.resources.empty_login import androiddev.apps.vbox.client.shared.generated.resources.empty_login
import androiddev.apps.vbox.client.shared.generated.resources.empty_pass import androiddev.apps.vbox.client.shared.generated.resources.empty_pass
import androiddev.apps.vbox.client.shared.generated.resources.error_title
import androiddev.apps.vbox.client.shared.generated.resources.okay
import androiddev.apps.vbox.client.shared.generated.resources.unknown_error import androiddev.apps.vbox.client.shared.generated.resources.unknown_error
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
@@ -38,13 +34,14 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.ErrorAlert
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.PasswordTextField import ru.shadowsparky.ui.components.PasswordTextField
import ru.shadowsparky.vbox.shared.domain.model.EmptyLoginException import ru.shadowsparky.vbox.shared.domain.model.EmptyLoginException
import ru.shadowsparky.vbox.shared.domain.model.EmptyPasswordException import ru.shadowsparky.vbox.shared.domain.model.EmptyPasswordException
import ru.shadowsparky.vbox.shared.domain.model.LoginException import ru.shadowsparky.vbox.shared.domain.model.LoginException
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.SimpleAppBar import ru.shadowsparky.vbox.shared.presentation.SimpleAppBar
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@Composable @Composable
fun AuthScreen(comp: AuthComponent) { fun AuthScreen(comp: AuthComponent) {
@@ -107,17 +104,9 @@ fun BaseLogin(
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp)
) { ) {
if (errorInfo != null && state is AuthState.Error) { if (errorInfo != null && state is AuthState.Error) {
AlertDialog( ErrorAlert(
onDismissRequest = { errorInfo = null }, errorInfo!!.toMessage(),
title = { Text(stringResource(Res.string.error_title)) }, onClick = { errorInfo = null }
text = { Text(errorInfo!!.toMessage()) },
confirmButton = {
Text(
text = stringResource(Res.string.okay),
modifier = Modifier.padding(8.dp)
.clickable { errorInfo = null }
)
},
) )
} }
OutlinedTextField( OutlinedTextField(
@@ -31,10 +31,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.PasswordTextField import ru.shadowsparky.ui.components.PasswordTextField
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.SimpleAppBar import ru.shadowsparky.vbox.shared.presentation.SimpleAppBar
import ru.shadowsparky.vbox.shared.presentation.list.Loading
import ru.shadowsparky.vbox.shared.presentation.settings.auth.AuthState import ru.shadowsparky.vbox.shared.presentation.settings.auth.AuthState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -14,7 +14,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -36,21 +35,18 @@ import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.BackButton import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.vbox.shared.presentation.BaseScreen import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable @Composable
fun EditServScreen(comp: EditServComponent) { fun EditServScreen(comp: EditServComponent) {
val url by comp.serverUrl.collectAsState(null) val url by comp.serverUrl.collectAsState(null)
LaunchedEffect(null) {
comp.state.collect {
if (it is EditServState.Succeed) {
comp.doBack()
}
}
}
val state by comp.state.collectAsState(null) val state by comp.state.collectAsState(null)
LaunchedEffect(state) {
if (state is EditServState.Succeed) comp.doBack()
}
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
BaseScreen( BaseScreen(
@@ -98,5 +94,6 @@ fun EditServScreen(comp: EditServComponent) {
} else { } else {
Loading() Loading()
} }
}) }
)
} }
@@ -1,35 +0,0 @@
package ru.shadowsparky.vbox.shared.presentation.state
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.saveable.rememberSaveable
import org.koin.core.annotation.Factory
@Factory
class TopAppBarStateHolder {
var heightOffsetLimit: Float = -Float.MAX_VALUE
var heightOffset: Float = 0f
var contentOffset: Float = 0f
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberTopAppBarStateByHolder(holder: TopAppBarStateHolder): TopAppBarState {
val state = rememberSaveable(saver = TopAppBarState.Saver) {
TopAppBarState(
holder.heightOffsetLimit,
holder.heightOffset,
holder.contentOffset
)
}
DisposableEffect(Unit) {
onDispose {
holder.heightOffsetLimit = state.heightOffsetLimit
holder.heightOffset = state.heightOffset
holder.contentOffset = state.contentOffset
}
}
return state
}
@@ -92,9 +92,7 @@ fun ChatContent(
} }
is ChatState.Error -> { is ChatState.Error -> {
Column( Column(horizontalAlignment = Alignment.CenterHorizontally) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text( Text(
text = stringResource(Res.string.chat_load_error), text = stringResource(Res.string.chat_load_error),
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
+1 -1
View File
@@ -1,6 +1,6 @@
#Tue Jun 23 20:20:20 MSK 2026 #Tue Jun 23 20:20:20 MSK 2026
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
@@ -0,0 +1,4 @@
<resources>
<string name="error_title">Произошла ошибка</string>
<string name="ok">Понятно</string>
</resources>
@@ -0,0 +1,93 @@
package ru.shadowsparky.ui.components
import androiddev.libs.ui.generated.resources.Res
import androiddev.libs.ui.generated.resources.error_title
import androiddev.libs.ui.generated.resources.ok
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
@Composable
fun ErrorContent(
message: String,
modifier: Modifier = Modifier,
buttonModifier: Modifier = Modifier,
buttonText: String = stringResource(Res.string.ok),
onClick: () -> Unit
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxSize().padding(4.dp)
) {
Text(
text = stringResource(Res.string.error_title),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(2.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(8.dp))
Button(modifier = buttonModifier, onClick = { onClick() }) {
Text(
text = buttonText,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun ErrorAlert(
message: String,
buttonText: String = stringResource(Res.string.ok),
onClick: () -> Unit,
onSkip: () -> Unit = onClick
) {
AlertContent(onSkip) {
ErrorContent(
message,
Modifier,
Modifier.fillMaxWidth(),
buttonText,
onClick
)
}
}
@Preview
@Composable
fun ErrorContentPreview() {
ErrorContent("что-то пошло не так") {
}
}
@Preview
@Composable
fun ErrorAlertPreview() {
ErrorAlert(
"Увы... Что-то опять идет не так...",
onClick = {
}
)
}
@@ -14,6 +14,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurSource
@Composable @Composable
expect fun isTv(): Boolean expect fun isTv(): Boolean
@@ -123,7 +125,11 @@ fun LazyColumn(
) { ) {
val isTv = isTv() val isTv = isTv()
androidx.compose.foundation.lazy.LazyColumn( androidx.compose.foundation.lazy.LazyColumn(
if (isTv) modifier.padding(contentPadding) else modifier, if (isTv) {
modifier.padding(contentPadding)
} else {
modifier.applyBlurSource(LocalBlurState.current)
},
state, state,
if (isTv) PaddingValues(0.dp) else contentPadding, if (isTv) PaddingValues(0.dp) else contentPadding,
reverseLayout, reverseLayout,
@@ -14,6 +14,8 @@ import androidx.compose.foundation.rememberOverscrollEffect
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.dp import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurSource
@Composable @Composable
fun LazyVerticalGrid( fun LazyVerticalGrid(
@@ -33,7 +35,11 @@ fun LazyVerticalGrid(
val isTv = isTv() val isTv = isTv()
androidx.compose.foundation.lazy.grid.LazyVerticalGrid( androidx.compose.foundation.lazy.grid.LazyVerticalGrid(
columns, columns,
if (isTv) modifier.padding(contentPadding) else modifier, if (isTv) {
modifier.padding(contentPadding)
} else {
modifier.applyBlurSource(LocalBlurState.current)
},
state, state,
if (isTv) PaddingValues(0.dp) else contentPadding, if (isTv) PaddingValues(0.dp) else contentPadding,
reverseLayout, reverseLayout,
@@ -1,6 +1,7 @@
package ru.shadowsparky.ui.components package ru.shadowsparky.ui.components
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LoadingIndicator
@@ -12,7 +13,7 @@ import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun Loading(modifier: Modifier) { fun Loading(modifier: Modifier = Modifier.fillMaxSize()) {
Box(modifier, contentAlignment = Alignment.Center) { Box(modifier, contentAlignment = Alignment.Center) {
LoadingIndicator(Modifier.size(96.dp)) LoadingIndicator(Modifier.size(96.dp))
} }
@@ -0,0 +1,42 @@
package ru.shadowsparky.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.theme.ComponentDefaults
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBar(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight,
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = ComponentDefaults.topAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null,
contentPadding: PaddingValues = TopAppBarDefaults.ContentPadding
) {
TopAppBar(
title = title,
modifier = modifier.applyBlurEffect(LocalBlurState.current),
navigationIcon = navigationIcon,
actions = actions,
expandedHeight = expandedHeight,
windowInsets = windowInsets,
colors = colors,
scrollBehavior = scrollBehavior,
contentPadding = contentPadding
)
}