add AlertContent, add RouteNavigator

This commit is contained in:
2026-07-26 20:25:05 +03:00
parent 38a93ed95e
commit 30b1acd3ff
39 changed files with 398 additions and 395 deletions
@@ -16,14 +16,14 @@ import kotlinx.coroutines.launch
import ru.shadowsparky.koin import ru.shadowsparky.koin
import ru.shadowsparky.ui.theme.LocalColorSchemeProvider import ru.shadowsparky.ui.theme.LocalColorSchemeProvider
import ru.shadowsparky.vbox.shared.presentation.Nav import ru.shadowsparky.vbox.shared.presentation.Nav
import ru.shadowsparky.vbox.shared.presentation.RootComponent
import ru.shadowsparky.vbox.shared.presentation.RootFactory import ru.shadowsparky.vbox.shared.presentation.RootFactory
import ru.shadowsparky.vbox.shared.presentation.UrlRoutes import ru.shadowsparky.vbox.shared.presentation.UrlRoutes
import ru.shadowsparky.vbox.shared.presentation.VBoxRootComponent
import ru.shadowsparky.vbox.shared.presentation.nav.Route import ru.shadowsparky.vbox.shared.presentation.nav.Route
import ru.shadowsparky.videobox.multiplatform.piggy.getPiggyColorScheme import ru.shadowsparky.videobox.multiplatform.piggy.getPiggyColorScheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var root: RootComponent private lateinit var root: VBoxRootComponent
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
edgeToEdge(false) edgeToEdge(false)
@@ -46,10 +46,10 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
if (isPiggyActivity) { if (isPiggyActivity) {
CompositionLocalProvider(LocalColorSchemeProvider provides getPiggyColorScheme()) { CompositionLocalProvider(LocalColorSchemeProvider provides getPiggyColorScheme()) {
Nav(rootComponent = root) Nav(root = root)
} }
} else { } else {
Nav(rootComponent = root) Nav(root = root)
} }
edgeToEdge(isSystemInDarkTheme()) edgeToEdge(isSystemInDarkTheme())
} }
@@ -42,15 +42,15 @@ fun Modifier.imePaddingInternal(): Modifier {
} }
@Composable @Composable
fun Nav(rootComponent: RootComponent) { fun Nav(root: VBoxRootComponent) {
CompositionLocalProvider( CompositionLocalProvider(
LocalImageLoader provides remember { rootComponent.imageLoaderProvider.provide() } LocalImageLoader provides remember { root.imageLoaderProvider.provide() }
) { NavInternal(rootComponent) } ) { NavInternal(root) }
} }
@Composable @Composable
private fun NavInternal(rootComponent: RootComponent) { private fun NavInternal(root: VBoxRootComponent) {
Nav(rootComponent) { child, route -> Nav(root) { child, route ->
when (child) { when (child) {
is Child.PlayerChild -> PlayerScreen(child.component) is Child.PlayerChild -> PlayerScreen(child.component)
is Child.OverviewChild -> OverviewScreen(child.component) is Child.OverviewChild -> OverviewScreen(child.component)
@@ -73,12 +73,12 @@ private fun NavInternal(rootComponent: RootComponent) {
MainScreen( MainScreen(
route as Route, route as Route,
child, child,
rootComponent root
) )
} }
} }
} }
UpdateDialog(rootComponent.updateComponent) UpdateDialog(root.updateComponent)
} }
} }
@@ -22,15 +22,15 @@ import ru.shadowsparky.vbox.shared.presentation.search.SearchScreen
import ru.shadowsparky.vbox.shared.presentation.settings.SettingsContent import ru.shadowsparky.vbox.shared.presentation.settings.SettingsContent
@Composable @Composable
fun MainScreen(route: Route, child: Child, rootComponent: RootComponent) { fun MainScreen(route: Route, child: Child, root: VBoxRootComponent) {
when (child) { when (child) {
is Child.SearchChild -> SearchScreen(child.component, route) { rootComponent.nav(it, true) } is Child.SearchChild -> SearchScreen(child.component, route) { root.nav(it, true) }
is Child.SavedMovieChild -> SavedMovieScreen( is Child.SavedMovieChild -> SavedMovieScreen(
child.component, child.component,
route route
) { rootComponent.navReplace(it) } ) { root.navReplace(it) }
else -> MainScreenInternal(route, child) { rootComponent.navReplace(it) } else -> MainScreenInternal(route, child) { root.navReplace(it) }
} }
} }
@@ -12,23 +12,23 @@ import kotlinx.serialization.KSerializer
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import ru.shadowsparky.http.domain.TokenStorage import ru.shadowsparky.http.domain.TokenStorage
import ru.shadowsparky.ui.nav.BaseRootComponent import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.updater.presentation.UpdateComponentFactory import ru.shadowsparky.updater.presentation.UpdateComponentFactory
import ru.shadowsparky.vbox.shared.di.factory.ImageLoaderProvider import ru.shadowsparky.vbox.shared.di.factory.ImageLoaderProvider
import ru.shadowsparky.vbox.shared.domain.HeathCheck import ru.shadowsparky.vbox.shared.domain.HeathCheck
import ru.shadowsparky.vbox.shared.presentation.nav.Route import ru.shadowsparky.vbox.shared.presentation.nav.Route
@OptIn(ExperimentalDecomposeApi::class) @OptIn(ExperimentalDecomposeApi::class)
class RootComponent( class VBoxRootComponent(
componentContext: ComponentContext, componentContext: ComponentContext,
tokenStorage: TokenStorage, tokenStorage: TokenStorage,
override val initStack: List<Route>, override val initStack: List<Route>,
private val healthCheck: HeathCheck, private val healthCheck: HeathCheck,
val imageLoaderProvider: ImageLoaderProvider, val imageLoaderProvider: ImageLoaderProvider,
private val updateComponentFactory: UpdateComponentFactory, updateComponentFactory: UpdateComponentFactory,
override val serializer: KSerializer<Route> = Route.serializer(), override val serializer: KSerializer<Route> = Route.serializer(),
override val koin: Koin = ru.shadowsparky.koin override val koin: Koin = ru.shadowsparky.koin
) : BaseRootComponent<Route>(componentContext), WebNavigationOwner { ) : RootComponent<Route>(componentContext), WebNavigationOwner {
val updateComponent = updateComponentFactory.create(childContext("UpdateComponentRoot")) val updateComponent = updateComponentFactory.create(childContext("UpdateComponentRoot"))
private val scope = createComponentScope() private val scope = createComponentScope()
val isLoggedIn = tokenStorage.token.map { val isLoggedIn = tokenStorage.token.map {
@@ -68,15 +68,15 @@ class RootFactory(
fun create( fun create(
context: ComponentContext, context: ComponentContext,
initStack: Route initStack: Route
): RootComponent { ): VBoxRootComponent {
return create(context, listOf(initStack)) return create(context, listOf(initStack))
} }
fun create( fun create(
context: ComponentContext, context: ComponentContext,
initStack: List<Route> = listOf(Route.default) initStack: List<Route> = listOf(Route.default)
): RootComponent { ): VBoxRootComponent {
return RootComponent( return VBoxRootComponent(
context, context,
tokenStorage, tokenStorage,
listOf(Route.Loading(next = initStack.toSet())), listOf(Route.Loading(next = initStack.toSet())),
@@ -4,6 +4,7 @@ import androiddev.apps.vbox.client.shared.generated.resources.Res
import androiddev.apps.vbox.client.shared.generated.resources.not_found import androiddev.apps.vbox.client.shared.generated.resources.not_found
import androiddev.apps.vbox.client.shared.generated.resources.recommendations import androiddev.apps.vbox.client.shared.generated.resources.recommendations
import androiddev.apps.vbox.client.shared.generated.resources.retry import androiddev.apps.vbox.client.shared.generated.resources.retry
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@@ -80,7 +81,6 @@ fun VideoScreen(component: VideoScreenComponent) {
onRefresh = { component.load() }, onRefresh = { component.load() },
onShowOverview = { component.showOverview(it) }, onShowOverview = { component.showOverview(it) },
emptyResource = Res.string.not_found, emptyResource = Res.string.not_found,
onLoad = { component.load() }
) )
}, },
appBar = { appBar = {
@@ -107,8 +107,7 @@ fun VideoContent(component: VideoScreenComponent, paddingValues: PaddingValues)
items, items,
onRefresh = { component.load() }, onRefresh = { component.load() },
onShowOverview = { component.showOverview(it) }, onShowOverview = { component.showOverview(it) },
emptyResource = Res.string.not_found, emptyResource = Res.string.not_found
onLoad = { component.load() }
) )
} }
@@ -119,20 +118,21 @@ fun VideoScreenContent(
items: List<VideoItem>?, items: List<VideoItem>?,
emptyResource: StringResource, emptyResource: StringResource,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onShowOverview: (Long) -> Unit, onShowOverview: (Long) -> Unit
onLoad: () -> Unit
) { ) {
LaunchedEffect(Unit) {
onLoad()
}
if (error != null) { if (error != null) {
ErrorAndRetry(error) { ErrorAndRetry(error) {
onRefresh() onRefresh()
} }
} else if (items != null) { } else if (items != null) {
AnimatedContent(
items,
content = {
VideosInfo(items = items, paddingValues, emptyResource) { VideosInfo(items = items, paddingValues, emptyResource) {
onShowOverview(it.id) onShowOverview(it.id)
} }
}
)
} else { } else {
Loading() Loading()
} }
@@ -9,8 +9,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
import ru.shadowsparky.vbox.shared.domain.VideoApi import ru.shadowsparky.vbox.shared.domain.VideoApi
import ru.shadowsparky.vbox.shared.domain.model.VideoItem import ru.shadowsparky.vbox.shared.domain.model.VideoItem
@@ -39,6 +39,8 @@ class VideoScreenComponent(
val items: StateFlow<List<VideoItem>?> = _items.asStateFlow() val items: StateFlow<List<VideoItem>?> = _items.asStateFlow()
val errorCode = _errorMsg.asSharedFlow() val errorCode = _errorMsg.asSharedFlow()
init { load() }
fun load() { fun load() {
componentScope.launch(dispatcherProvider.main + exceptionHandler) { componentScope.launch(dispatcherProvider.main + exceptionHandler) {
_errorMsg.emit(null) _errorMsg.emit(null)
@@ -58,7 +60,7 @@ class VideosChildFactory(
override fun create( override fun create(
route: Route.Videos, route: Route.Videos,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.VideosChild { ): Child.VideosChild {
val comp = VideoScreenComponent( val comp = VideoScreenComponent(
child, child,
@@ -6,8 +6,8 @@ import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.http.domain.TokenStorage import ru.shadowsparky.http.domain.TokenStorage
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
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
@@ -26,7 +26,7 @@ class LoadingComponentFactory(
override fun create( override fun create(
route: Route.Loading, route: Route.Loading,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.LoadingChild { ): Child.LoadingChild {
return Child.LoadingChild( return Child.LoadingChild(
LoadingComponent( LoadingComponent(
@@ -7,7 +7,7 @@ import ru.shadowsparky.vbox.shared.presentation.overview.OverviewComponent
import ru.shadowsparky.vbox.shared.presentation.overview.episode.EpisodeOverviewComponent import ru.shadowsparky.vbox.shared.presentation.overview.episode.EpisodeOverviewComponent
import ru.shadowsparky.vbox.shared.presentation.overview.season.SeasonOverviewComponent import ru.shadowsparky.vbox.shared.presentation.overview.season.SeasonOverviewComponent
import ru.shadowsparky.vbox.shared.presentation.player.PlayerComponent import ru.shadowsparky.vbox.shared.presentation.player.PlayerComponent
import ru.shadowsparky.vbox.shared.presentation.saved.SavedMovieScreenComponent import ru.shadowsparky.vbox.shared.presentation.saved.SavedMovieComponent
import ru.shadowsparky.vbox.shared.presentation.search.SearchScreenComponent import ru.shadowsparky.vbox.shared.presentation.search.SearchScreenComponent
import ru.shadowsparky.vbox.shared.presentation.settings.SettingsComponent import ru.shadowsparky.vbox.shared.presentation.settings.SettingsComponent
import ru.shadowsparky.vbox.shared.presentation.settings.auth.AuthComponent import ru.shadowsparky.vbox.shared.presentation.settings.auth.AuthComponent
@@ -27,7 +27,7 @@ sealed class Child : ru.shadowsparky.ui.nav.Child {
class EditServerAddrChild(val component: EditServComponent) : Child() class EditServerAddrChild(val component: EditServComponent) : Child()
class AuthChild(val component: AuthComponent) : Child() class AuthChild(val component: AuthComponent) : Child()
class RegisterChild(val component: RegisterComponent) : Child() class RegisterChild(val component: RegisterComponent) : Child()
class SavedMovieChild(val component: SavedMovieScreenComponent) : Child() class SavedMovieChild(val component: SavedMovieComponent) : Child()
class EditPasswordChild(val component: EditPasswordComponent) : Child() class EditPasswordChild(val component: EditPasswordComponent) : Child()
class LoadingChild(val component: LoadingComponent) : Child() class LoadingChild(val component: LoadingComponent) : Child()
class PlayerChild(val component: PlayerComponent) : Child() class PlayerChild(val component: PlayerComponent) : Child()
@@ -23,7 +23,7 @@ sealed class Route : ru.shadowsparky.ui.nav.Route() {
@Serializable @Serializable
data class Loading( data class Loading(
val authRequired: Boolean = isJs, val authRequired: Boolean = true,
val next: Collection<Route> = listOf(default), val next: Collection<Route> = listOf(default),
override val routeId: String = RouteIds.LOADING override val routeId: String = RouteIds.LOADING
) : Route() ) : Route()
@@ -4,8 +4,8 @@ import com.arkivanov.decompose.ComponentContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
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.OfflineVideoInteractor import ru.shadowsparky.vbox.shared.domain.OfflineVideoInteractor
@@ -50,7 +50,7 @@ class OfflineVideoComponentFactory(
override fun create( override fun create(
route: Route.OfflineVideo, route: Route.OfflineVideo,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.OfflineVideoChild { ): Child.OfflineVideoChild {
val comp = OfflineVideoComponent( val comp = OfflineVideoComponent(
child, child,
@@ -15,8 +15,9 @@ import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.domain.Clip import ru.shadowsparky.domain.Clip
import ru.shadowsparky.domain.ClipboardManager import ru.shadowsparky.domain.ClipboardManager
import ru.shadowsparky.ui.nav.BaseRootComponent import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
import ru.shadowsparky.vbox.shared.domain.AddOfflineVideoUseCase import ru.shadowsparky.vbox.shared.domain.AddOfflineVideoUseCase
import ru.shadowsparky.vbox.shared.domain.OfflineStorage import ru.shadowsparky.vbox.shared.domain.OfflineStorage
@@ -165,7 +166,7 @@ class OverviewFactory(
override fun create( override fun create(
route: Route.Overview, route: Route.Overview,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.OverviewChild { ): Child.OverviewChild {
val comp = OverviewComponent( val comp = OverviewComponent(
savedMovieRepository, savedMovieRepository,
@@ -20,8 +20,9 @@ import org.koin.core.annotation.Named
import ru.shadowsparky.domain.Clip import ru.shadowsparky.domain.Clip
import ru.shadowsparky.domain.ClipboardManager import ru.shadowsparky.domain.ClipboardManager
import ru.shadowsparky.domain.DispatcherProvider import ru.shadowsparky.domain.DispatcherProvider
import ru.shadowsparky.ui.nav.BaseRootComponent import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.domain.AddOfflineVideoUseCase import ru.shadowsparky.vbox.shared.domain.AddOfflineVideoUseCase
import ru.shadowsparky.vbox.shared.domain.OfflineStorage import ru.shadowsparky.vbox.shared.domain.OfflineStorage
import ru.shadowsparky.vbox.shared.domain.OfflineVideo import ru.shadowsparky.vbox.shared.domain.OfflineVideo
@@ -178,7 +179,7 @@ class EpisodeOverviewFactory(
override fun create( override fun create(
route: Route.Overview.Episode, route: Route.Overview.Episode,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.EpisodeOverviewChild { ): Child.EpisodeOverviewChild {
val comp = EpisodeOverviewComponent( val comp = EpisodeOverviewComponent(
child, child,
@@ -8,8 +8,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
import ru.shadowsparky.vbox.shared.domain.VideoApi import ru.shadowsparky.vbox.shared.domain.VideoApi
import ru.shadowsparky.vbox.shared.domain.model.Season import ru.shadowsparky.vbox.shared.domain.model.Season
@@ -54,7 +54,7 @@ class SeasonOverviewFactory(
override fun create( override fun create(
route: Route.Overview.Season, route: Route.Overview.Season,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.SeasonOverviewChild { ): Child.SeasonOverviewChild {
val comp = SeasonOverviewComponent( val comp = SeasonOverviewComponent(
videoApi, videoApi,
@@ -9,8 +9,9 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.domain.PlayerInteractor import ru.shadowsparky.vbox.shared.domain.PlayerInteractor
import ru.shadowsparky.vbox.shared.presentation.createComponentScope import ru.shadowsparky.vbox.shared.presentation.createComponentScope
import ru.shadowsparky.vbox.shared.presentation.nav.Child import ru.shadowsparky.vbox.shared.presentation.nav.Child
@@ -112,7 +113,7 @@ class PlayerComponentFactory(
override fun create( override fun create(
route: Route.Overview.Player, route: Route.Overview.Player,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.PlayerChild { ): Child.PlayerChild {
val component = PlayerComponent( val component = PlayerComponent(
child, child,
@@ -8,8 +8,9 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
import ru.shadowsparky.vbox.shared.domain.model.VideoItem import ru.shadowsparky.vbox.shared.domain.model.VideoItem
@@ -19,7 +20,7 @@ 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
class SavedMovieScreenComponent( class SavedMovieComponent(
private val context: ComponentContext, private val context: ComponentContext,
repo: SavedMovieRepository, repo: SavedMovieRepository,
userTagRepository: UserTagRepository, userTagRepository: UserTagRepository,
@@ -70,9 +71,9 @@ class SavedMovieFactory(
override fun create( override fun create(
route: Route.Bookmark, route: Route.Bookmark,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.SavedMovieChild { ): Child.SavedMovieChild {
val comp = SavedMovieScreenComponent( val comp = SavedMovieComponent(
child, child,
repo, repo,
doBack = { root.onBack() }, doBack = { root.onBack() },
@@ -7,21 +7,16 @@ import androiddev.apps.vbox.client.shared.generated.resources.saved_movies_not_f
import androiddev.apps.vbox.client.shared.generated.resources.user_tags import androiddev.apps.vbox.client.shared.generated.resources.user_tags
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_filter_disable import androiddev.apps.vbox.client.shared.generated.resources.user_tags_filter_disable
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
@@ -38,6 +33,7 @@ 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.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect import ru.shadowsparky.ui.applyBlurEffect
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.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.domain.tag.TagInfo import ru.shadowsparky.vbox.shared.domain.tag.TagInfo
@@ -47,7 +43,7 @@ import ru.shadowsparky.vbox.shared.presentation.nav.Route
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SavedMovieScreen(comp: SavedMovieScreenComponent, route: Route, onNavigate: (Route) -> Unit) { fun SavedMovieScreen(comp: SavedMovieComponent, route: Route, onNavigate: (Route) -> Unit) {
val tags by comp.userTags.collectAsStateWithLifecycle(null) val tags by comp.userTags.collectAsStateWithLifecycle(null)
var filterBottomSheetVisible by rememberSaveable { mutableStateOf(false) } var filterBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
BaseMainScreen( BaseMainScreen(
@@ -86,7 +82,7 @@ fun SavedMovieScreen(comp: SavedMovieScreenComponent, route: Route, onNavigate:
} }
@Composable @Composable
private fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: PaddingValues) { private fun SavedMovieContent(comp: SavedMovieComponent, paddingValues: PaddingValues) {
val items by comp.all.collectAsStateWithLifecycle(null) val items by comp.all.collectAsStateWithLifecycle(null)
VideoScreenContent( VideoScreenContent(
paddingValues, paddingValues,
@@ -94,8 +90,7 @@ private fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: Pa
items, items,
onRefresh = {}, onRefresh = {},
onShowOverview = { comp.showOverview(it) }, onShowOverview = { comp.showOverview(it) },
emptyResource = Res.string.saved_movies_not_found, emptyResource = Res.string.saved_movies_not_found
onLoad = {}
) )
} }
@@ -108,15 +103,10 @@ private fun FilterDialog(
onFilterBy: (String?) -> Unit onFilterBy: (String?) -> Unit
) { ) {
allTags ?: return allTags ?: return
BasicAlertDialog(onDismissRequest = onDismiss) { AlertContent(
Surface(shape = MaterialTheme.shapes.extraLarge) { onDismiss,
Column(modifier = Modifier.padding(16.dp)) { title = stringResource(Res.string.user_tags)
Text( ) {
text = stringResource(Res.string.user_tags),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(Modifier.height(16.dp))
LazyColumn(modifier = Modifier.fillMaxWidth()) { LazyColumn(modifier = Modifier.fillMaxWidth()) {
item { item {
FilterItemCheckBox( FilterItemCheckBox(
@@ -131,7 +121,6 @@ private fun FilterDialog(
items = allTags.filter { it.id != null }, items = allTags.filter { it.id != null },
key = { tag -> tag.id ?: tag.tag } key = { tag -> tag.id ?: tag.tag }
) { tag -> ) { tag ->
Spacer(Modifier.height(4.dp))
FilterItemCheckBox( FilterItemCheckBox(
isChecked = filterTag == tag.tag, isChecked = filterTag == tag.tag,
name = tag.tag name = tag.tag
@@ -143,8 +132,6 @@ private fun FilterDialog(
} }
} }
} }
}
}
@Composable @Composable
private fun FilterItemCheckBox( private fun FilterItemCheckBox(
@@ -4,8 +4,9 @@ import com.arkivanov.decompose.ComponentContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.domain.SearchRepository import ru.shadowsparky.vbox.shared.domain.SearchRepository
import ru.shadowsparky.vbox.shared.presentation.createComponentScope import ru.shadowsparky.vbox.shared.presentation.createComponentScope
import ru.shadowsparky.vbox.shared.presentation.nav.Child import ru.shadowsparky.vbox.shared.presentation.nav.Child
@@ -43,7 +44,7 @@ class SearchScreenFactory(
override fun create( override fun create(
route: Route.SearchVideos, route: Route.SearchVideos,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.SearchChild { ): Child.SearchChild {
val comp = SearchScreenComponent( val comp = SearchScreenComponent(
child, child,
@@ -9,8 +9,8 @@ import org.koin.core.annotation.Named
import ru.shadowsparky.domain.AppConfig import ru.shadowsparky.domain.AppConfig
import ru.shadowsparky.http.domain.LogoutAction import ru.shadowsparky.http.domain.LogoutAction
import ru.shadowsparky.http.domain.TokenStorage import ru.shadowsparky.http.domain.TokenStorage
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.updater.presentation.UpdateComponentFactory import ru.shadowsparky.updater.presentation.UpdateComponentFactory
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
import ru.shadowsparky.vbox.shared.domain.OfflineStorage import ru.shadowsparky.vbox.shared.domain.OfflineStorage
@@ -83,7 +83,7 @@ class SettingsComponentFactory(
override fun create( override fun create(
route: Route.Settings, route: Route.Settings,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.SettingsChild { ): Child.SettingsChild {
val comp = SettingsComponent( val comp = SettingsComponent(
child, child,
@@ -7,8 +7,8 @@ import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.domain.Log import ru.shadowsparky.domain.Log
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository
import ru.shadowsparky.vbox.shared.domain.model.LoginException import ru.shadowsparky.vbox.shared.domain.model.LoginException
@@ -64,7 +64,7 @@ class AuthComponentFactory(
override fun create( override fun create(
route: Route.Settings.Auth, route: Route.Settings.Auth,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.AuthChild { ): Child.AuthChild {
val comp = AuthComponent( val comp = AuthComponent(
route.required, route.required,
@@ -6,8 +6,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository
import ru.shadowsparky.vbox.shared.domain.ChangePasswordRequest import ru.shadowsparky.vbox.shared.domain.ChangePasswordRequest
import ru.shadowsparky.vbox.shared.domain.model.LoginException import ru.shadowsparky.vbox.shared.domain.model.LoginException
@@ -53,7 +54,7 @@ class EditPasswordComponentFactory(
override fun create( override fun create(
route: Route.Settings.EditPassword, route: Route.Settings.EditPassword,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.EditPasswordChild { ): Child.EditPasswordChild {
val component = EditPasswordComponent( val component = EditPasswordComponent(
child, child,
@@ -8,8 +8,8 @@ import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import ru.shadowsparky.domain.Log import ru.shadowsparky.domain.Log
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository
import ru.shadowsparky.vbox.shared.domain.model.LoginException import ru.shadowsparky.vbox.shared.domain.model.LoginException
@@ -55,7 +55,7 @@ class RegisterComponentFactory(
override fun create( override fun create(
route: Route.Settings.Register, route: Route.Settings.Register,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.RegisterChild { ): Child.RegisterChild {
val comp = RegisterComponent( val comp = RegisterComponent(
child, child,
@@ -9,8 +9,9 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
import ru.shadowsparky.vbox.shared.domain.ServerConfiguration import ru.shadowsparky.vbox.shared.domain.ServerConfiguration
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
@@ -27,7 +28,6 @@ class EditServComponent(
) : ComponentContext by context { ) : ComponentContext by context {
private val componentScope = createComponentScope() private val componentScope = createComponentScope()
val serverUrl = serverConfigurationRepository.serverConfiguration val serverUrl = serverConfigurationRepository.serverConfiguration
.filterNotNull()
.map { it.asHttpStr() } .map { it.asHttpStr() }
.onEach { onUrlChanged(it) } .onEach { onUrlChanged(it) }
private val _state = MutableStateFlow<EditServState>(EditServState.Idle) private val _state = MutableStateFlow<EditServState>(EditServState.Idle)
@@ -67,7 +67,7 @@ class EditServComponentFactory(
override fun create( override fun create(
route: Route.Settings.EditServ, route: Route.Settings.EditServ,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.EditServerAddrChild { ): Child.EditServerAddrChild {
val comp = EditServComponent( val comp = EditServComponent(
child, child,
@@ -4,8 +4,6 @@ import androiddev.apps.vbox.client.shared.generated.resources.Res
import androiddev.apps.vbox.client.shared.generated.resources.movie_tags_empty import androiddev.apps.vbox.client.shared.generated.resources.movie_tags_empty
import androiddev.apps.vbox.client.shared.generated.resources.user_tags import androiddev.apps.vbox.client.shared.generated.resources.user_tags
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -14,15 +12,14 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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 org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.AlertContent
import ru.shadowsparky.ui.components.LazyColumn import ru.shadowsparky.ui.components.LazyColumn
import ru.shadowsparky.ui.components.Loading import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.vbox.shared.domain.tag.TagInfo import ru.shadowsparky.vbox.shared.domain.tag.TagInfo
@@ -36,18 +33,10 @@ fun LinkTagBottomSheet(
requestTagsScreen: () -> Unit, requestTagsScreen: () -> Unit,
updateTag: (String, Boolean) -> Unit updateTag: (String, Boolean) -> Unit
) { ) {
ModalBottomSheet( AlertContent(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(true) title = stringResource(Res.string.user_tags)
) { ) {
Column(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(Res.string.user_tags),
style = MaterialTheme.typography.titleLarge
)
if (allTags == null || linkedTags == null) { if (allTags == null || linkedTags == null) {
Loading(Modifier.fillMaxWidth()) Loading(Modifier.fillMaxWidth())
} else { } else {
@@ -67,7 +56,6 @@ fun LinkTagBottomSheet(
} }
} }
} }
}
@Composable @Composable
private fun ShowTags( private fun ShowTags(
@@ -79,28 +67,22 @@ private fun ShowTags(
linkedTags.mapNotNull { it.id }.toSet() linkedTags.mapNotNull { it.id }.toSet()
} }
LazyColumn( LazyColumn(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items( items(
items = allTags.filter { it.id != null }, items = allTags.filter { it.id != null },
key = { tag -> tag.id ?: tag.tag } key = { tag -> tag.id ?: tag.tag }
) { tag -> ) { tag ->
val tagId = tag.id ?: return@items val tagId = tag.id ?: return@items
val isChecked = tagId in linkedTagIds val isChecked = tagId in linkedTagIds
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { updateTag(tag.tag, !isChecked) } .clickable { updateTag(tag.tag, !isChecked) },
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = tag.tag, text = tag.tag,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f).padding(start = 8.dp),
.padding(start = 8.dp),
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
Checkbox( Checkbox(
@@ -4,8 +4,8 @@ import com.arkivanov.decompose.ComponentContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import ru.shadowsparky.ui.nav.BaseRootComponent
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
import ru.shadowsparky.vbox.shared.presentation.createComponentScope import ru.shadowsparky.vbox.shared.presentation.createComponentScope
import ru.shadowsparky.vbox.shared.presentation.nav.Child import ru.shadowsparky.vbox.shared.presentation.nav.Child
@@ -48,7 +48,7 @@ class TagComponentFactory(
override fun create( override fun create(
route: Route.Settings.TagOverview, route: Route.Settings.TagOverview,
child: ComponentContext, child: ComponentContext,
root: BaseRootComponent<Route> root: RouteNavigator<Route>
): Child.TagChild { ): Child.TagChild {
val component = TagComponent( val component = TagComponent(
child, child,
@@ -9,7 +9,6 @@ import androiddev.apps.vbox.client.shared.generated.resources.user_tags_new_tag_
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_save import androiddev.apps.vbox.client.shared.generated.resources.user_tags_save
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_tag_delete import androiddev.apps.vbox.client.shared.generated.resources.user_tags_tag_delete
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -17,12 +16,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -39,6 +35,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.AlertContent
import ru.shadowsparky.ui.components.ExpressiveLazyColumn import ru.shadowsparky.ui.components.ExpressiveLazyColumn
import ru.shadowsparky.ui.components.TextPreference import ru.shadowsparky.ui.components.TextPreference
import ru.shadowsparky.vbox.shared.domain.tag.TagInfo import ru.shadowsparky.vbox.shared.domain.tag.TagInfo
@@ -122,18 +119,10 @@ private fun AddTagBottomSheet(
keyboardController?.show() keyboardController?.show()
} }
ModalBottomSheet( AlertContent(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(true) title = stringResource(Res.string.user_tags_add)
) { ) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = stringResource(Res.string.user_tags_add),
style = MaterialTheme.typography.titleLarge
)
TextField( TextField(
value = tag, value = tag,
onValueChange = { tag = it }, onValueChange = { tag = it },
@@ -152,7 +141,6 @@ private fun AddTagBottomSheet(
) { Text(stringResource(Res.string.user_tags_save)) } ) { Text(stringResource(Res.string.user_tags_save)) }
} }
} }
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -169,15 +157,10 @@ private fun EditTagBottomSheet(
val tagId = tag.id val tagId = tag.id
ModalBottomSheet(onDismissRequest = onDismiss) { AlertContent(
Column( onDismissRequest = onDismiss,
modifier = Modifier.fillMaxWidth().padding(8.dp), title = stringResource(Res.string.user_tags_edit_title)
verticalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Text(
text = stringResource(Res.string.user_tags_edit_title),
style = MaterialTheme.typography.titleLarge
)
TextField( TextField(
value = newTag, value = newTag,
onValueChange = { newTag = it }, onValueChange = { newTag = it },
@@ -209,4 +192,3 @@ private fun EditTagBottomSheet(
) { Text(stringResource(Res.string.user_tags_tag_delete)) } ) { Text(stringResource(Res.string.user_tags_tag_delete)) }
} }
} }
}
@@ -30,9 +30,9 @@ import ru.shadowsparky.koin
import ru.shadowsparky.ui.hideElementById import ru.shadowsparky.ui.hideElementById
import ru.shadowsparky.ui.hookPageVisibility import ru.shadowsparky.ui.hookPageVisibility
import ru.shadowsparky.vbox.shared.presentation.Nav import ru.shadowsparky.vbox.shared.presentation.Nav
import ru.shadowsparky.vbox.shared.presentation.RootComponent
import ru.shadowsparky.vbox.shared.presentation.RootFactory import ru.shadowsparky.vbox.shared.presentation.RootFactory
import ru.shadowsparky.vbox.shared.presentation.UrlRoutes import ru.shadowsparky.vbox.shared.presentation.UrlRoutes
import ru.shadowsparky.vbox.shared.presentation.VBoxRootComponent
import ru.shadowsparky.vbox.shared.presentation.nav.Route import ru.shadowsparky.vbox.shared.presentation.nav.Route
@KoinApplication @KoinApplication
@@ -67,9 +67,9 @@ fun mainInternal() {
val composeRoot = document.getElementById("compose-root") as HTMLElement val composeRoot = document.getElementById("compose-root") as HTMLElement
var rootRef: RootComponent? = null var rootRef: VBoxRootComponent? = null
val root: RootComponent = withWebHistory { stateKeeper: StateKeeper, deepLinkUrl: String? -> val root: VBoxRootComponent = withWebHistory { stateKeeper: StateKeeper, deepLinkUrl: String? ->
if (rootRef == null) { if (rootRef == null) {
rootRef = createRoot(lifecycle, stateKeeper, deepLinkUrl) rootRef = createRoot(lifecycle, stateKeeper, deepLinkUrl)
} }
@@ -103,7 +103,7 @@ private fun createRoot(
lifecycle: LifecycleRegistry, lifecycle: LifecycleRegistry,
stateKeeper: StateKeeper, stateKeeper: StateKeeper,
deepLinkUrl: String?, deepLinkUrl: String?,
): RootComponent { ): VBoxRootComponent {
val ctx = DefaultComponentContext( val ctx = DefaultComponentContext(
lifecycle = lifecycle, stateKeeper = stateKeeper lifecycle = lifecycle, stateKeeper = stateKeeper
) )
+5
View File
@@ -12,8 +12,13 @@ kotlin {
dependencies { dependencies {
implementation(libs.haze) implementation(libs.haze)
implementation(libs.haze.materials) implementation(libs.haze.materials)
implementation(libs.ui.tooling.preview)
} }
} }
} }
android { namespace = "ru.shadowsparky.ui" } android { namespace = "ru.shadowsparky.ui" }
} }
dependencies {
androidRuntimeClasspath(libs.ui.tooling)
}
@@ -0,0 +1,69 @@
package ru.shadowsparky.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlertContent(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
canDismiss: Boolean = true,
content: @Composable () -> Unit,
) {
@Composable
fun BaseDialogLayout() {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
title?.let {
Text(
text = it,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
)
}
content()
}
}
if (isTv()) {
BasicAlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier,
properties = DialogProperties(
dismissOnBackPress = canDismiss,
dismissOnClickOutside = canDismiss
)
) {
Surface(
shape = MaterialTheme.shapes.extraLarge,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 6.dp
) { BaseDialogLayout() }
}
} else {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = canDismiss,
shouldDismissOnClickOutside = canDismiss
),
shape = MaterialTheme.shapes.extraLarge
) { BaseDialogLayout() }
}
}
@@ -8,6 +8,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@Composable @Composable
@@ -22,3 +23,9 @@ fun BackButton(doBack: () -> Unit) {
) )
) { Icon(painterResource(Res.drawable.back), contentDescription = null) } ) { Icon(painterResource(Res.drawable.back), contentDescription = null) }
} }
@Preview
@Composable
private fun BackButtonPreview() {
BackButton {}
}
@@ -11,6 +11,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -139,3 +140,23 @@ fun ExpressiveCard(
modifier = modifier.padding(horizontal = horizontalPadding, vertical = verticalSpacing) modifier = modifier.padding(horizontal = horizontalPadding, vertical = verticalSpacing)
) { content() } ) { content() }
} }
@Composable
@Preview
private fun ExpressiveLazyColumnPreview() {
ExpressiveLazyColumn {
section {
item {
TextPreference(text = "item one")
}
item {
TextPreference(text = "item two")
}
}
section {
item {
TextPreference(text = "next section started")
}
}
}
}
@@ -1,66 +0,0 @@
package ru.shadowsparky.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> ListPreference(
title: String,
items: Map<T, String>,
selectedItem: T,
onItemSelected: (T) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
var expanded by rememberSaveable { mutableStateOf(false) }
val entries = remember(items) { items.entries.toList() }
val selectedLabel = remember(items, selectedItem) { items[selectedItem].orEmpty() }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { if (enabled) expanded = !expanded },
modifier = modifier
) {
OutlinedTextField(
value = selectedLabel,
onValueChange = {}, // readOnly
readOnly = true,
label = { Text(title) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled)
.fillMaxWidth(),
enabled = enabled,
singleLine = true
)
ExposedDropdownMenu(
expanded = expanded && enabled,
onDismissRequest = { expanded = false }
) {
entries.forEach { (value, label) ->
DropdownMenuItem(
text = { Text(label) },
onClick = {
expanded = false
if (value != selectedItem) onItemSelected(value)
}
)
}
}
}
}
@@ -7,6 +7,7 @@ import androidx.compose.material3.LoadingIndicator
import androidx.compose.runtime.Composable 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -16,3 +17,9 @@ fun Loading(modifier: Modifier) {
LoadingIndicator(Modifier.size(96.dp)) LoadingIndicator(Modifier.size(96.dp))
} }
} }
@Preview
@Composable
fun LoadingPreview() {
Loading(Modifier)
}
@@ -60,4 +60,3 @@ fun PasswordTextField(
} }
) )
} }
@@ -1,27 +0,0 @@
package ru.shadowsparky.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun PreferenceCategory(
title: String,
modifier: Modifier = Modifier,
) {
Text(
text = title,
modifier = modifier
.padding(
start = 16.dp,
top = 24.dp,
end = 16.dp,
bottom = 8.dp,
),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleSmall,
)
}
@@ -3,5 +3,5 @@ package ru.shadowsparky.ui.nav
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
interface ComponentFactory<T : Child, R : Route, BaseRoute : Route> { interface ComponentFactory<T : Child, R : Route, BaseRoute : Route> {
fun create(route: R, child: ComponentContext, root: BaseRootComponent<BaseRoute>): T fun create(route: R, child: ComponentContext, root: RouteNavigator<BaseRoute>): T
} }
@@ -14,7 +14,7 @@ import ru.shadowsparky.ui.theme.getColorScheme
@OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun Nav( fun Nav(
rootComponent: BaseRootComponent<*>, rootComponent: RootComponent<*>,
process: @Composable (child: Child, route: Route) -> Unit process: @Composable (child: Child, route: Route) -> Unit
) { ) {
Children( Children(
@@ -18,18 +18,18 @@ import kotlinx.serialization.KSerializer
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
abstract class BaseRootComponent<R : Route>( abstract class RootComponent<R : Route>(
private val componentContext: ComponentContext, private val componentContext: ComponentContext,
) : ComponentContext by componentContext, BackHandlerOwner { ) : ComponentContext by componentContext, BackHandlerOwner, RouteNavigator<R> {
protected abstract val initStack: List<R> protected abstract val initStack: List<R>
protected abstract val serializer: KSerializer<R> protected abstract val serializer: KSerializer<R>
protected abstract val koin: Koin protected abstract val koin: Koin
private val scope = coroutineScope() private val scope = coroutineScope()
protected val nav = StackNavigation<R>() protected val nav = StackNavigation<R>()
protected val childStack by lazy { val childStack by lazy {
childStack( childStack(
source = nav, source = nav,
serializer = serializer, serializer = serializer,
@@ -41,36 +41,26 @@ abstract class BaseRootComponent<R : Route>(
private val _onFinish = MutableStateFlow<Unit?>(null) private val _onFinish = MutableStateFlow<Unit?>(null)
val onFinish = _onFinish.filterNotNull() val onFinish = _onFinish.filterNotNull()
fun navReplace(vararg route: R) { override fun navReplace(vararg route: R) {
nav.replaceAll(*route) nav.replaceAll(*route)
} }
fun nav(route: R, bringToFront: Boolean = false) { override fun nav(route: R, bringToFront: Boolean) {
if (bringToFront) { if (bringToFront) nav.bringToFront(route) else nav.pushNew(route)
nav.bringToFront(route)
} else {
nav.pushNew(route)
}
} }
fun onBack() { override fun onBack() {
nav.pop { popped -> nav.pop { popped -> if (!popped) finish() }
if (!popped) finish()
}
} }
fun finish() { override fun finish() {
scope.launch { _onFinish.emit(Unit) } scope.launch { _onFinish.emit(Unit) }
} }
val valueChildStack: Value<ChildStack<*, Child>> by lazy { childStack } val valueChildStack: Value<ChildStack<*, Child>> by lazy { childStack }
protected open fun newChild(route: Route, ctx: ComponentContext): Child { protected open fun newChild(route: Route, ctx: ComponentContext): Child {
val factory = route.provideFactory<Child, Route>(koin) val factory = koin.get<ComponentFactory<Child, Route, R>>(named(route.routeId))
return factory.create(route, ctx, this as BaseRootComponent<Route>) return factory.create(route, ctx, this as RouteNavigator<R>)
}
private fun <T : Child, R : Route> Route.provideFactory(koin: Koin): ComponentFactory<T, R, Route> {
return koin.get<ComponentFactory<T, R, Route>>(named(routeId))
} }
} }
@@ -0,0 +1,8 @@
package ru.shadowsparky.ui.nav
interface RouteNavigator<out R : Route> {
fun navReplace(vararg route: @UnsafeVariance R)
fun nav(route: @UnsafeVariance R, bringToFront: Boolean = false)
fun onBack()
fun finish()
}
@@ -18,18 +18,14 @@ import androiddev.libs.updater.updater_client.generated.resources.updater_title_
import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission
import androiddev.libs.updater.updater_client.generated.resources.updater_title_ready import androiddev.libs.updater.updater_client.generated.resources.updater_title_ready
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -38,10 +34,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
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 org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.AlertContent
import ru.shadowsparky.ui.components.Loading import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.updater.domain.UpdateState import ru.shadowsparky.updater.domain.UpdateState
@@ -55,16 +51,11 @@ fun UpdateDialog(
val canDismiss = remember(state) { !component.isForceUpdate } val canDismiss = remember(state) { !component.isForceUpdate }
val isVisible = remember(state) { state !is UpdateState.Idle } val isVisible = remember(state) { state !is UpdateState.Idle }
if (isVisible) { if (isVisible) {
BasicAlertDialog( AlertContent(
modifier = modifier, modifier = modifier,
onDismissRequest = { component.reset() }, onDismissRequest = { component.reset() },
properties = DialogProperties( canDismiss = canDismiss,
canDismiss,
canDismiss,
),
content = { content = {
Surface(shape = MaterialTheme.shapes.extraLarge) {
Box(modifier = Modifier.padding(16.dp)) {
when (val currentState = state) { when (val currentState = state) {
is UpdateState.Checking -> UpdateCheckingContent() is UpdateState.Checking -> UpdateCheckingContent()
is UpdateState.UpdateAvailable -> UpdateAvailableContent( is UpdateState.UpdateAvailable -> UpdateAvailableContent(
@@ -72,35 +63,40 @@ fun UpdateDialog(
changeLog = currentState.changeLog, changeLog = currentState.changeLog,
onDownloadClick = { component.downloadUpdate(currentState.url) } onDownloadClick = { component.downloadUpdate(currentState.url) }
) )
is UpdateState.InstallPermissionRequired -> UpdatePermissionContent( is UpdateState.InstallPermissionRequired -> UpdatePermissionContent(
onGrantClick = { component.requestPermission() } onGrantClick = { component.requestPermission() }
) )
is UpdateState.Downloading -> UpdateDownloadingContent( is UpdateState.Downloading -> UpdateDownloadingContent(
progress = currentState.progress, progress = currentState.progress,
downloadedBytes = currentState.downloadedBytes, downloadedBytes = currentState.downloadedBytes,
totalBytes = currentState.totalBytes totalBytes = currentState.totalBytes
) )
is UpdateState.ReadyToInstall -> UpdateReadyContent( is UpdateState.ReadyToInstall -> UpdateReadyContent(
onInstallClick = { component.installApk(currentState.filePath) } onInstallClick = { component.installApk(currentState.filePath) }
) )
is UpdateState.Error -> UpdateErrorContent( is UpdateState.Error -> UpdateErrorContent(
message = currentState.message, message = currentState.message,
onRetryClick = { component.checkUpdates() } onRetryClick = { component.checkUpdates() }
) )
is UpdateState.UpToDate -> { is UpdateState.UpToDate -> {
UpdateFinishedContent(onSkip = { component.reset() }) UpdateFinishedContent(onSkip = { component.reset() })
} }
is UpdateState.Completed -> { is UpdateState.Completed -> {
UpdateFinishedContent( UpdateFinishedContent(
Res.string.updater_title_completed, Res.string.updater_title_completed,
onSkip = { component.reset() } onSkip = { component.reset() }
) )
} }
else -> Unit else -> Unit
} }
} }
}
}
) )
} }
} }
@@ -138,7 +134,10 @@ private fun UpdateAvailableContent(
) )
} }
Button(onClick = onDownloadClick, modifier = Modifier.fillMaxWidth()) { Button(onClick = onDownloadClick, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(Res.string.updater_btn_download), style = MaterialTheme.typography.labelLarge) Text(
stringResource(Res.string.updater_btn_download),
style = MaterialTheme.typography.labelLarge
)
} }
} }
} }
@@ -153,10 +152,19 @@ private fun UpdatePermissionContent(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text(text = stringResource(Res.string.updater_title_permission), style = MaterialTheme.typography.titleLarge) Text(
Text(text = stringResource(Res.string.updater_desc_permission), style = MaterialTheme.typography.bodyMedium) text = stringResource(Res.string.updater_title_permission),
style = MaterialTheme.typography.titleLarge
)
Text(
text = stringResource(Res.string.updater_desc_permission),
style = MaterialTheme.typography.bodyMedium
)
Button(onClick = onGrantClick, modifier = Modifier.fillMaxWidth()) { Button(onClick = onGrantClick, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(Res.string.updater_btn_grant), style = MaterialTheme.typography.labelLarge) Text(
stringResource(Res.string.updater_btn_grant),
style = MaterialTheme.typography.labelLarge
)
} }
} }
} }
@@ -173,14 +181,21 @@ private fun UpdateDownloadingContent(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text(text = stringResource(Res.string.updater_title_downloading), style = MaterialTheme.typography.titleMedium) Text(
text = stringResource(Res.string.updater_title_downloading),
style = MaterialTheme.typography.titleMedium
)
LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth())
val downloadedMb = ((downloadedBytes / (1024f * 1024f)) * 10).toInt() / 10.0 val downloadedMb = ((downloadedBytes / (1024f * 1024f)) * 10).toInt() / 10.0
val totalMb = ((totalBytes / (1024f * 1024f)) * 10).toInt() / 10.0 val totalMb = ((totalBytes / (1024f * 1024f)) * 10).toInt() / 10.0
Text( Text(
text = stringResource(Res.string.updater_download_progress, downloadedMb.toString(), totalMb.toString()), text = stringResource(
Res.string.updater_download_progress,
downloadedMb.toString(),
totalMb.toString()
),
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
@@ -196,9 +211,15 @@ private fun UpdateReadyContent(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text(text = stringResource(Res.string.updater_title_ready), style = MaterialTheme.typography.titleLarge) Text(
text = stringResource(Res.string.updater_title_ready),
style = MaterialTheme.typography.titleLarge
)
Button(onClick = onInstallClick, modifier = Modifier.fillMaxWidth()) { Button(onClick = onInstallClick, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(Res.string.updater_btn_install), style = MaterialTheme.typography.labelLarge) Text(
stringResource(Res.string.updater_btn_install),
style = MaterialTheme.typography.labelLarge
)
} }
} }
} }
@@ -216,7 +237,10 @@ private fun UpdateFinishedContent(
) { ) {
Text(text = stringResource(text), style = MaterialTheme.typography.titleLarge) Text(text = stringResource(text), style = MaterialTheme.typography.titleLarge)
Button(onClick = onSkip, modifier = Modifier.fillMaxWidth()) { Button(onClick = onSkip, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(Res.string.updater_btn_close), style = MaterialTheme.typography.labelLarge) Text(
stringResource(Res.string.updater_btn_close),
style = MaterialTheme.typography.labelLarge
)
} }
} }
} }
@@ -232,10 +256,17 @@ private fun UpdateErrorContent(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text(text = stringResource(Res.string.updater_title_error), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.error) Text(
text = stringResource(Res.string.updater_title_error),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.error
)
Text(text = message, style = MaterialTheme.typography.bodyMedium) Text(text = message, style = MaterialTheme.typography.bodyMedium)
Button(onClick = onRetryClick, modifier = Modifier.fillMaxWidth()) { Button(onClick = onRetryClick, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(Res.string.updater_btn_retry), style = MaterialTheme.typography.labelLarge) Text(
stringResource(Res.string.updater_btn_retry),
style = MaterialTheme.typography.labelLarge
)
} }
} }
} }