From 757b5c61bc56247e8d5638fc07b03ce2c31cd859 Mon Sep 17 00:00:00 2001 From: Toby Bridle Date: Sun, 26 Nov 2023 15:45:49 +0000 Subject: [PATCH] =?UTF-8?q?feat(WatchView):=20=F0=9F=8E=89=20add=20server?= =?UTF-8?q?=20selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screens/watch/WatchView.kt | 154 ++++++++++++++++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 139 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/chouten/app/presentation/ui/screens/watch/WatchView.kt b/app/src/main/java/com/chouten/app/presentation/ui/screens/watch/WatchView.kt index 31f911d..1601bd7 100644 --- a/app/src/main/java/com/chouten/app/presentation/ui/screens/watch/WatchView.kt +++ b/app/src/main/java/com/chouten/app/presentation/ui/screens/watch/WatchView.kt @@ -3,10 +3,32 @@ package com.chouten.app.presentation.ui.screens.watch import android.content.Intent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -18,16 +40,23 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewModelScope +import com.chouten.app.R import com.chouten.app.common.Navigation +import com.chouten.app.common.UiText import com.chouten.app.domain.model.SnackbarModel import com.chouten.app.presentation.ui.screens.watch.WatchViewModel.Companion.STATUS import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -48,6 +77,7 @@ data class WatchBundle( val mediaTitle: String, ) +@OptIn(ExperimentalMaterial3Api::class) @Composable @Destination( route = Navigation.WatchRoute @@ -71,7 +101,7 @@ fun WatchView( STATUS, WatchViewModelState.DEFAULT ).collectAsState() - val selectedServer by rememberSaveable { mutableIntStateOf(0) } + var selectedServer by rememberSaveable { mutableIntStateOf(0) } val selectedSource by rememberSaveable { mutableIntStateOf(0) } var servers by remember { mutableStateOf?>(null) } @@ -104,21 +134,15 @@ fun WatchView( bundle.selectedMediaIndex ) } - } else { - if (servers == null) { - // We want to use the viewmodel scope here since we don't - // want the server handler to be cancelled when the view is recomposed - watchViewModel.viewModelScope.launch { - context.cacheDir.resolve("${bundle.mediaUuid}_server.json").useLines { lines -> - val text = lines.joinToString("\n") - val result = Json.decodeFromString>(text) - servers = result - } + } else if (servers == null) { + // We want to use the viewmodel scope here since we don't + // want the server handler to be cancelled when the view is recomposed + watchViewModel.viewModelScope.launch { + context.cacheDir.resolve("${bundle.mediaUuid}_server.json").useLines { lines -> + val text = lines.joinToString("\n") + val result = Json.decodeFromString>(text) + servers = result } - } else { - val serverUrl = - servers?.getOrNull(selectedServer)?.list?.getOrNull(selectedSource)?.url - watchViewModel.getSource(serverUrl ?: "") } } @@ -135,13 +159,111 @@ fun WatchView( Box( modifier = Modifier .fillMaxSize() + .navigationBarsPadding() .background(Color.Black) ) { if (!status.isSourceSet) { CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) + modifier = Modifier + .align(Alignment.Center) ) } + AnimatedVisibility(visible = servers != null, enter = fadeIn()) { + val sourceLambda: suspend () -> Unit = { + val serverUrl = + servers?.getOrNull(selectedServer)?.list?.getOrNull( + selectedSource + )?.url + watchViewModel.getSource(serverUrl ?: "") + } + if (servers?.size == 1) { + runBlocking { + sourceLambda() + } + return@AnimatedVisibility + } + BottomSheetScaffold( + scaffoldState = BottomSheetScaffoldState( + bottomSheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = SheetValue.Expanded + ), + snackbarHostState = SnackbarHostState() + ), + modifier = Modifier.fillMaxSize(), + sheetPeekHeight = 150.dp, + sheetContent = { + servers?.let { + if (it.isEmpty()) return@let null + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(it) { index, serverData -> + ListItem( + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + 12.dp + ) + ), + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .clickable { + runBlocking { sourceLambda() } + }, + headlineContent = { + Text( + serverData.title.trim(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + supportingContent = { + Text( + "${serverData.list.firstOrNull()?.name?.trim()}", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + leadingContent = { + Text( + "#${index + 1}", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + trailingContent = { + Text("${serverData.list.size} Sources") + } + ) + } + } + } ?: run { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "(×﹏×)", + fontSize = MaterialTheme.typography.headlineLarge.fontSize + ) + Text( + UiText.StringRes((R.string.no_servers_found)) + .string(), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(horizontal = 16.dp), + textAlign = TextAlign.Center + ) + } + } + } + ) {} + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf98adf..ab57bc3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,4 +64,5 @@ Module Auto-Update Could not auto-update %1$s Attempting to auto update %1$s + Whoops.. Looks like no servers were found? \ No newline at end of file