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.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -14,6 +13,7 @@ import androidx.compose.ui.Modifier
import com.seiko.imageloader.LocalImageLoader
import ru.shadowsparky.koin
import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.ui.nav.Nav
import ru.shadowsparky.updater.presentation.UpdateDialog
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.material3.ExperimentalMaterial3Api
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.CompositionLocalProvider
import androidx.compose.ui.Modifier
import ru.shadowsparky.ui.AppScaffold
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.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.presentation.chat.ChatScreen
import ru.shadowsparky.vbox.shared.presentation.list.VideoContent
import ru.shadowsparky.vbox.shared.presentation.nav.Child
@@ -40,7 +35,6 @@ fun MainScreen(route: Route, child: Child, root: VBoxRootComponent) {
fun BaseMainScreen(
appBar: @Composable () -> Unit,
route: Route,
scrollBehavior: TopAppBarScrollBehavior,
onNavigate: (Route) -> Unit,
content: @Composable (PaddingValues) -> Unit
) {
@@ -51,8 +45,7 @@ fun BaseMainScreen(
currentRoute = route,
onSelectNavigation = { onNavigate(it) },
navigationEntries = createNavigationList(),
colorScheme = getColorScheme(),
scrollBehavior = scrollBehavior
colorScheme = getColorScheme()
) { padding ->
content(padding)
}
@@ -63,20 +56,15 @@ fun BaseMainScreen(
@Composable
private fun MainScreenInternal(route: Route, child: Child, onNavigate: (Route) -> Unit) {
val navList = createNavigationList()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
BaseMainScreen(
appBar = {
navList.findTitle(route)?.let { title ->
TopAppBar(
title = { Text(title) },
scrollBehavior = scrollBehavior,
modifier = Modifier.applyBlurEffect(LocalBlurState.current),
colors = ComponentDefaults.topAppBarColors()
title = { Text(title) }
)
}
},
route = route,
scrollBehavior = scrollBehavior,
onNavigate = onNavigate,
content = { padding ->
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 androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.chat.presentation.ChatComponent
import ru.shadowsparky.chat.presentation.ChatContent
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.ui.components.TopAppBar
import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen
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()
BaseMainScreen(
appBar = {
TopAppBar(
title = { Text(stringResource(Res.string.chat_title_app_bar)) },
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(LocalBlurState.current)
)
TopAppBar(title = { Text(stringResource(Res.string.chat_title_app_bar)) })
},
route,
onNavigate = onNavigate,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
content = { ChatContent(component, it, state) }
)
}
@@ -17,7 +17,6 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
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.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -57,19 +54,17 @@ import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.LazyVerticalGrid
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.rememberBlurState
import ru.shadowsparky.vbox.shared.domain.model.VideoItem
import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.search.AppBarWithSearch
import ru.shadowsparky.vbox.shared.presentation.state.rememberTopAppBarStateByHolder
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoScreen(component: VideoScreenComponent) {
val appBarState = rememberTopAppBarStateByHolder(component.topAppBarStateHolder)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(appBarState)
val error by component.errorCode.collectAsState(null)
val items by component.items.collectAsState()
val blurState = rememberBlurState()
@@ -93,8 +88,7 @@ fun VideoScreen(component: VideoScreenComponent) {
isDefault = title == appName,
onBack = { component.doBack() }
)
},
scrollBehavior = scrollBehavior
}
)
}
}
@@ -123,9 +117,10 @@ fun VideoScreenContent(
onShowOverview: (Long) -> Unit
) {
if (error != null) {
ErrorAndRetry(error) {
onRefresh()
}
ErrorContent(
error,
buttonText = stringResource(Res.string.retry)
) { onRefresh() }
} else if (items != null) {
AnimatedContent(
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
fun VideosNotFound(emptyResource: StringResource) {
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.Route
import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
import ru.shadowsparky.vbox.shared.presentation.state.TopAppBarStateHolder
class VideoScreenComponent(
private val context: ComponentContext,
private val videoApi: VideoApi,
private val dispatcherProvider: DispatcherProvider,
val topAppBarStateHolder: TopAppBarStateHolder,
val search: String? = null,
val showOverview: (Long) -> Unit,
val doBack: () -> Unit
@@ -53,7 +51,6 @@ class VideoScreenComponent(
@Factory
@Named(RouteIds.VIDEOS)
class VideosChildFactory(
private val topAppBarStateHolder: TopAppBarStateHolder,
private val videoApi: VideoApi,
private val dispatcherProvider: DispatcherProvider
) : ComponentFactory<Child.VideosChild, Route.Videos, Route> {
@@ -66,7 +63,6 @@ class VideosChildFactory(
child,
videoApi,
dispatcherProvider,
topAppBarStateHolder,
route.request,
showOverview = { root.nav(Route.Overview(it)) },
doBack = { root.onBack() }
@@ -5,8 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@OptIn(ExperimentalMaterial3Api::class)
@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.offline_remove
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.star
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.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
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.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -72,26 +69,23 @@ import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.domain.Clip
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.ErrorContent
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.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
import ru.shadowsparky.vbox.shared.presentation.BaseScreen
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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OverviewScreen(component: OverviewComponent) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val errorState by component.errorCode.collectAsState(initial = null)
val details by component.details.collectAsState()
val snackbarState = remember { SnackbarHostState() }
val blurState = rememberBlurState()
var tagsBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
CompositionLocalProvider(LocalBlurState provides blurState) {
@@ -101,17 +95,15 @@ fun OverviewScreen(component: OverviewComponent) {
component.offlineSupport,
details,
component,
scrollBehavior,
tagsRequest = { tagsBottomSheetVisible = true }
)
},
scrollBehavior = scrollBehavior,
snackbarState = snackbarState,
content = { padding ->
when {
errorState != null -> ErrorAndRetry(
errorMsg = errorState!!
errorState != null -> ErrorContent(
errorState!!,
buttonText = stringResource(Res.string.retry)
) { component.loadDetailsIfNeeded() }
details != null -> DetailsInfo(
@@ -145,12 +137,10 @@ private fun OverviewAppBar(
offlineSupport: Boolean,
details: VideoDetails?,
component: OverviewComponent,
scrollBehavior: TopAppBarScrollBehavior,
tagsRequest: () -> Unit
) {
val isSaved by component.isSaved.collectAsStateWithLifecycle(null)
val offlineItem by component.offlineItem.collectAsStateWithLifecycle(null)
val blur = LocalBlurState.current
TopAppBar(
title = {
details?.title?.let {
@@ -162,7 +152,6 @@ private fun OverviewAppBar(
}
},
navigationIcon = { BackButton { component.doBack() } },
colors = ComponentDefaults.topAppBarColors(),
actions = {
details?.let {
if (component.shareManager.canShare()) {
@@ -192,9 +181,7 @@ private fun OverviewAppBar(
tagsRequest
) { 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.folder_check
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.select_all
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -26,8 +27,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -47,19 +46,18 @@ import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.domain.Clip
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.ErrorContent
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.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.domain.OfflineState
import ru.shadowsparky.vbox.shared.domain.OfflineVideo
import ru.shadowsparky.vbox.shared.domain.model.Episode
import ru.shadowsparky.vbox.shared.presentation.BaseScreen
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)
@Composable
@@ -96,7 +94,6 @@ fun EpisodesOverviewContent(
startOffline: () -> Unit,
commitOffline: (Boolean) -> Unit
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val snackbarState = remember { SnackbarHostState() }
val blurState = rememberBlurState()
var isSaveMode by rememberSaveable { mutableStateOf(false) }
@@ -133,9 +130,6 @@ fun EpisodesOverviewContent(
}
}
},
scrollBehavior = scrollBehavior,
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(blurState),
actions = {
if (offlineSupport) {
IconButton(
@@ -165,8 +159,7 @@ fun EpisodesOverviewContent(
}
}
)
},
scrollBehavior = scrollBehavior
}
)
}
@@ -184,7 +177,10 @@ fun EpisodesOverviewList(
onOfflineClicked: (EpisodeWithOfflineInfo) -> Unit
) {
if (errorCode != null) {
ErrorAndRetry(errorMsg = errorCode) { loadLinks() }
ErrorContent(
errorCode,
buttonText = stringResource(Res.string.retry)
) { loadLinks() }
} else if (episodes != null) {
val blur = LocalBlurState.current
val requester = remember { FocusRequester() }
@@ -34,6 +34,8 @@ class SeasonOverviewComponent(
val error = _error.asSharedFlow()
private val exceptionHandler = createExceptionHandler(componentScope) { _error.emit(it) }
init { load() }
fun load() {
componentScope.launch(dispatchers.main + exceptionHandler) {
_error.emit(null)
@@ -1,6 +1,7 @@
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.retry
import androiddev.apps.vbox.client.shared.generated.resources.seasons_overview
import androidx.compose.foundation.clickable
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -22,22 +21,19 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.applyBlurSource
import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.components.ErrorContent
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.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.domain.model.Season
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)
@Composable
fun SeasonOverviewScreen(component: SeasonOverviewComponent) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
LaunchedEffect(null) { component.load() }
val blurState = rememberBlurState()
CompositionLocalProvider(LocalBlurState provides blurState) {
BaseScreen(
@@ -45,9 +41,10 @@ fun SeasonOverviewScreen(component: SeasonOverviewComponent) {
val seasons = component.season.collectAsState(null)
val err = component.error.collectAsState(null)
if (err.value != null) {
ErrorAndRetry(err.value!!) {
component.load()
}
ErrorContent(
err.value!!,
buttonText = stringResource(Res.string.retry)
) { component.load() }
} else if (seasons.value != null) {
SeasonsInfo(
seasons.value!!, it
@@ -61,11 +58,9 @@ fun SeasonOverviewScreen(component: SeasonOverviewComponent) {
Text(stringResource(Res.string.seasons_overview))
}, navigationIcon = {
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.next_episode
import androiddev.apps.vbox.client.shared.generated.resources.retry
import androiddev.apps.vbox.client.shared.generated.resources.start_from
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -9,7 +10,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -28,8 +28,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.AppScaffold
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.vbox.shared.presentation.list.ErrorAndRetry
import ru.shadowsparky.videoplayer.PlayerEvent
import ru.shadowsparky.videoplayer.ui.VideoPlayer
@@ -47,12 +47,11 @@ fun PlayerScreen(component: PlayerComponent) {
when (val state = e.state) {
is PlayerEvent.Idle -> Loading(Modifier.fillMaxSize())
is PlayerEvent.Error -> {
Column(modifier = Modifier.fillMaxSize().padding(it)) {
ErrorAndRetry(
ErrorContent(
state.message,
retry = { component.load() }
)
}
modifier = Modifier.padding(it),
buttonText = stringResource(Res.string.retry),
) { component.load() }
}
is PlayerEvent.Ended -> component.requestNextEpisode()
@@ -18,8 +18,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -31,11 +29,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.painterResource
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.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.presentation.BaseMainScreen
import ru.shadowsparky.vbox.shared.presentation.list.VideoScreenContent
@@ -59,13 +55,10 @@ fun SavedMovieScreen(comp: SavedMovieComponent, route: Route, onNavigate: (Route
contentDescription = null
)
}
},
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(LocalBlurState.current)
}
)
},
route = route,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
onNavigate = onNavigate,
content = {
if (filterBottomSheetVisible) {
@@ -103,7 +103,6 @@ fun SearchScreen(
) { textFieldValue = it }
},
route = route,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
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.empty_login
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 androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
@@ -38,13 +34,14 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
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.vbox.shared.domain.model.EmptyLoginException
import ru.shadowsparky.vbox.shared.domain.model.EmptyPasswordException
import ru.shadowsparky.vbox.shared.domain.model.LoginException
import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.SimpleAppBar
import ru.shadowsparky.vbox.shared.presentation.list.Loading
@Composable
fun AuthScreen(comp: AuthComponent) {
@@ -107,17 +104,9 @@ fun BaseLogin(
modifier = Modifier.padding(8.dp)
) {
if (errorInfo != null && state is AuthState.Error) {
AlertDialog(
onDismissRequest = { errorInfo = null },
title = { Text(stringResource(Res.string.error_title)) },
text = { Text(errorInfo!!.toMessage()) },
confirmButton = {
Text(
text = stringResource(Res.string.okay),
modifier = Modifier.padding(8.dp)
.clickable { errorInfo = null }
)
},
ErrorAlert(
errorInfo!!.toMessage(),
onClick = { errorInfo = null }
)
}
OutlinedTextField(
@@ -31,10 +31,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.PasswordTextField
import ru.shadowsparky.vbox.shared.presentation.BaseScreen
import ru.shadowsparky.vbox.shared.presentation.SimpleAppBar
import ru.shadowsparky.vbox.shared.presentation.list.Loading
import ru.shadowsparky.vbox.shared.presentation.settings.auth.AuthState
@OptIn(ExperimentalMaterial3Api::class)
@@ -14,7 +14,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.stringResource
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.list.Loading
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun EditServScreen(comp: EditServComponent) {
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)
LaunchedEffect(state) {
if (state is EditServState.Succeed) comp.doBack()
}
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }
BaseScreen(
@@ -98,5 +94,6 @@ fun EditServScreen(comp: EditServComponent) {
} else {
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 -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(Res.string.chat_load_error),
color = MaterialTheme.colorScheme.error
+1 -1
View File
@@ -1,6 +1,6 @@
#Tue Jun 23 20:20:20 MSK 2026
distributionBase=GRADLE_USER_HOME
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
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.Modifier
import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurSource
@Composable
expect fun isTv(): Boolean
@@ -123,7 +125,11 @@ fun LazyColumn(
) {
val isTv = isTv()
androidx.compose.foundation.lazy.LazyColumn(
if (isTv) modifier.padding(contentPadding) else modifier,
if (isTv) {
modifier.padding(contentPadding)
} else {
modifier.applyBlurSource(LocalBlurState.current)
},
state,
if (isTv) PaddingValues(0.dp) else contentPadding,
reverseLayout,
@@ -14,6 +14,8 @@ import androidx.compose.foundation.rememberOverscrollEffect
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.applyBlurSource
@Composable
fun LazyVerticalGrid(
@@ -33,7 +35,11 @@ fun LazyVerticalGrid(
val isTv = isTv()
androidx.compose.foundation.lazy.grid.LazyVerticalGrid(
columns,
if (isTv) modifier.padding(contentPadding) else modifier,
if (isTv) {
modifier.padding(contentPadding)
} else {
modifier.applyBlurSource(LocalBlurState.current)
},
state,
if (isTv) PaddingValues(0.dp) else contentPadding,
reverseLayout,
@@ -1,6 +1,7 @@
package ru.shadowsparky.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
@@ -12,7 +13,7 @@ import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun Loading(modifier: Modifier) {
fun Loading(modifier: Modifier = Modifier.fillMaxSize()) {
Box(modifier, contentAlignment = Alignment.Center) {
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
)
}