mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-09 08:27:54 +00:00
Merge branch 'nav3' into cmp-rewrite
This commit is contained in:
commit
d622d1384c
32 changed files with 2746 additions and 1182 deletions
|
|
@ -423,7 +423,7 @@ kotlin {
|
|||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.atomicfu)
|
||||
implementation(libs.kmpalette.core)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.supabase.postgrest)
|
||||
implementation(libs.supabase.auth)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -11,3 +13,7 @@ internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
|
|||
internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 6.dp
|
||||
@Composable
|
||||
internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets.navigationBars
|
||||
|
||||
@Composable
|
||||
internal actual fun platformPhysicalTopInset(): Dp =
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -72,6 +72,8 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import com.nuvio.app.navigation.LocalNativeNavigationBarHidden
|
||||
import com.nuvio.app.navigation.LocalUseNativeNavigation
|
||||
|
||||
@Composable
|
||||
fun NuvioScreen(
|
||||
|
|
@ -142,6 +144,20 @@ fun NuvioScreenHeader(
|
|||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
val nativeDetailNavigation = LocalUseNativeNavigation.current &&
|
||||
!LocalNativeNavigationBarHidden.current &&
|
||||
onBack != null
|
||||
if (nativeDetailNavigation) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = NuvioTokens.Space.s4),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = actions,
|
||||
)
|
||||
return
|
||||
}
|
||||
val resolvedTopPadding = topPadding ?: if (includeStatusBarPadding) statusBarTop else NuvioTokens.Space.none
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
|
|
@ -263,6 +279,8 @@ fun NuvioBackButton(
|
|||
iconSize: Dp = NuvioTokens.Icon.md,
|
||||
contentDescription: String = stringResource(Res.string.action_back),
|
||||
) {
|
||||
if (LocalUseNativeNavigation.current && !LocalNativeNavigationBarHidden.current) return
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(buttonSize)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import com.nuvio.app.features.profiles.AvatarRepository
|
||||
import com.nuvio.app.features.profiles.AvatarCatalogItem
|
||||
import com.nuvio.app.features.profiles.MAX_PROFILES
|
||||
import com.nuvio.app.features.profiles.NuvioProfile
|
||||
import com.nuvio.app.features.profiles.PinVerifyResult
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import com.nuvio.app.features.profiles.profileAvatarImageUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal enum class NativeNavigationTab {
|
||||
Home,
|
||||
|
|
@ -20,17 +35,11 @@ internal enum class NativeNavigationTab {
|
|||
internal object NativeTabBridge {
|
||||
private val _requestedTabs = MutableSharedFlow<NativeNavigationTab>(extraBufferCapacity = 1)
|
||||
val requestedTabs: SharedFlow<NativeNavigationTab> = _requestedTabs.asSharedFlow()
|
||||
private val _profileTabLongPresses = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
val profileTabLongPresses: SharedFlow<Unit> = _profileTabLongPresses.asSharedFlow()
|
||||
|
||||
fun requestTab(tabName: String) {
|
||||
_requestedTabs.tryEmit(NativeNavigationTab.fromName(tabName))
|
||||
}
|
||||
|
||||
fun requestProfileTabLongPress() {
|
||||
_profileTabLongPresses.tryEmit(Unit)
|
||||
}
|
||||
|
||||
fun publishSelectedTab(tab: NativeNavigationTab) {
|
||||
publishNativeSelectedTab(tab.name)
|
||||
}
|
||||
|
|
@ -71,12 +80,113 @@ internal object NativeTabBridge {
|
|||
}
|
||||
}
|
||||
|
||||
fun nativeTabSelect(tabName: String) {
|
||||
NativeTabBridge.requestTab(tabName)
|
||||
data class NativeProfileOption(
|
||||
val profileIndex: Int,
|
||||
val name: String,
|
||||
val avatarColorHex: String,
|
||||
val avatarImageUrl: String?,
|
||||
val avatarBackgroundColorHex: String?,
|
||||
val pinEnabled: Boolean,
|
||||
val active: Boolean,
|
||||
)
|
||||
|
||||
data class NativeProfileSwitcherState(
|
||||
val profiles: List<NativeProfileOption>,
|
||||
val isLoaded: Boolean,
|
||||
val canAddProfile: Boolean,
|
||||
)
|
||||
|
||||
class NativeProfileSwitcherController {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private val profileSelections = Channel<Int>(Channel.BUFFERED)
|
||||
private val manageProfileRequests = Channel<Unit>(Channel.BUFFERED)
|
||||
private var observationJob: Job? = null
|
||||
|
||||
internal val selectedProfileIndices = profileSelections.receiveAsFlow()
|
||||
internal val requestedManageProfiles = manageProfileRequests.receiveAsFlow()
|
||||
|
||||
fun currentState(): NativeProfileSwitcherState = nativeState(
|
||||
profilesLoaded = ProfileRepository.state.value.isLoaded,
|
||||
profiles = ProfileRepository.state.value.profiles,
|
||||
activeProfileIndex = ProfileRepository.state.value.activeProfile?.profileIndex,
|
||||
avatarsById = AvatarRepository.avatars.value.associateBy { it.id },
|
||||
)
|
||||
|
||||
fun observeState(callback: (NativeProfileSwitcherState) -> Unit) {
|
||||
observationJob?.cancel()
|
||||
observationJob = scope.launch {
|
||||
combine(ProfileRepository.state, AvatarRepository.avatars) { state, avatars ->
|
||||
nativeState(
|
||||
profilesLoaded = state.isLoaded,
|
||||
profiles = state.profiles,
|
||||
activeProfileIndex = state.activeProfile?.profileIndex,
|
||||
avatarsById = avatars.associateBy { it.id },
|
||||
)
|
||||
}.collect { callback(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun stopObserving() {
|
||||
observationJob?.cancel()
|
||||
observationJob = null
|
||||
}
|
||||
|
||||
private fun nativeState(
|
||||
profilesLoaded: Boolean,
|
||||
profiles: List<NuvioProfile>,
|
||||
activeProfileIndex: Int?,
|
||||
avatarsById: Map<String, AvatarCatalogItem>,
|
||||
): NativeProfileSwitcherState {
|
||||
val options = profiles.map { profile ->
|
||||
val avatar = profile.avatarId?.let(avatarsById::get)
|
||||
NativeProfileOption(
|
||||
profileIndex = profile.profileIndex,
|
||||
name = profile.name,
|
||||
avatarColorHex = profile.avatarColorHex,
|
||||
avatarImageUrl = profileAvatarImageUrl(profile, avatar),
|
||||
avatarBackgroundColorHex = avatar?.bgColor,
|
||||
pinEnabled = profile.pinEnabled,
|
||||
active = profile.profileIndex == activeProfileIndex,
|
||||
)
|
||||
}
|
||||
return NativeProfileSwitcherState(
|
||||
profiles = options,
|
||||
isLoaded = profilesLoaded,
|
||||
canAddProfile = profiles.size < MAX_PROFILES,
|
||||
)
|
||||
}
|
||||
|
||||
fun chooseProfile(
|
||||
profileIndex: Int,
|
||||
pin: String?,
|
||||
completion: (PinVerifyResult) -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
val profile = ProfileRepository.state.value.profiles
|
||||
.firstOrNull { it.profileIndex == profileIndex }
|
||||
if (profile == null) {
|
||||
completion(PinVerifyResult(message = null))
|
||||
return@launch
|
||||
}
|
||||
val result = if (profile.pinEnabled) {
|
||||
ProfileRepository.verifyPin(profileIndex, pin.orEmpty())
|
||||
} else {
|
||||
PinVerifyResult(unlocked = true)
|
||||
}
|
||||
if (result.unlocked) {
|
||||
profileSelections.trySend(profileIndex)
|
||||
}
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestManageProfiles() {
|
||||
manageProfileRequests.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun nativeProfileTabLongPress() {
|
||||
NativeTabBridge.requestProfileTabLongPress()
|
||||
fun nativeTabSelect(tabName: String) {
|
||||
NativeTabBridge.requestTab(tabName)
|
||||
}
|
||||
|
||||
internal expect fun isLiquidGlassNativeTabBarSupported(): Boolean
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ internal expect val nuvioBottomNavigationExtraVerticalPadding: Dp
|
|||
@Composable
|
||||
internal expect fun nuvioBottomNavigationBarInsets(): WindowInsets
|
||||
|
||||
/** Physical display-safe top inset, excluding any enclosing native toolbar. */
|
||||
@Composable
|
||||
internal expect fun platformPhysicalTopInset(): Dp
|
||||
|
||||
internal val LocalNuvioBottomNavigationOverlayPadding = staticCompositionLocalOf { 0.dp }
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
|
|
@ -58,6 +61,7 @@ import com.nuvio.app.features.home.PosterShape
|
|||
import com.nuvio.app.features.home.stableKey
|
||||
import com.nuvio.app.features.watched.WatchedRepository
|
||||
import com.nuvio.app.features.watching.application.WatchingState
|
||||
import com.nuvio.app.navigation.LocalUseNativeNavigation
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -235,6 +239,16 @@ private fun CatalogHeader(
|
|||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (LocalUseNativeNavigation.current) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets.statusBars)
|
||||
.height(44.dp),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ data class CollectionEditorUiState(
|
|||
val traktTrendingResults: List<TraktPublicListSearchResult> = emptyList(),
|
||||
val traktPopularResults: List<TraktPublicListSearchResult> = emptyList(),
|
||||
val traktSearchError: String? = null,
|
||||
val sourcePickerCompletionGeneration: Long = 0L,
|
||||
)
|
||||
|
||||
enum class TmdbBuilderMode {
|
||||
|
|
@ -725,6 +726,7 @@ object CollectionEditorRepository {
|
|||
tmdbCompanyResults = emptyList(),
|
||||
tmdbCollectionResults = emptyList(),
|
||||
tmdbSearchError = null,
|
||||
sourcePickerCompletionGeneration = _uiState.value.sourcePickerCompletionGeneration + 1L,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -833,6 +835,7 @@ object CollectionEditorRepository {
|
|||
traktTitleInput = "",
|
||||
traktSearchResults = emptyList(),
|
||||
traktSearchError = null,
|
||||
sourcePickerCompletionGeneration = state.sourcePickerCompletionGeneration + 1L,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -75,47 +76,87 @@ import sh.calvin.reorderable.ReorderableCollectionItemScope
|
|||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
enum class CollectionEditorPage {
|
||||
Root,
|
||||
FolderEditor,
|
||||
CatalogPicker,
|
||||
TmdbSourcePicker,
|
||||
TraktSourcePicker,
|
||||
}
|
||||
|
||||
fun disposeCollectionEditorPage(page: CollectionEditorPage) {
|
||||
when (page) {
|
||||
CollectionEditorPage.Root -> Unit
|
||||
CollectionEditorPage.FolderEditor -> CollectionEditorRepository.cancelFolderEdit()
|
||||
CollectionEditorPage.CatalogPicker -> CollectionEditorRepository.hideCatalogPicker()
|
||||
CollectionEditorPage.TmdbSourcePicker -> CollectionEditorRepository.hideTmdbSourcePicker()
|
||||
CollectionEditorPage.TraktSourcePicker -> CollectionEditorRepository.hideTraktSourcePicker()
|
||||
}
|
||||
}
|
||||
|
||||
private val autoDismissedPickerPages = setOf(
|
||||
CollectionEditorPage.TmdbSourcePicker,
|
||||
CollectionEditorPage.TraktSourcePicker,
|
||||
)
|
||||
|
||||
private fun CollectionEditorUiState.activeEditorPage(): CollectionEditorPage = when {
|
||||
showCatalogPicker -> CollectionEditorPage.CatalogPicker
|
||||
showTmdbSourcePicker -> CollectionEditorPage.TmdbSourcePicker
|
||||
showTraktSourcePicker -> CollectionEditorPage.TraktSourcePicker
|
||||
showFolderEditor && editingFolder != null -> CollectionEditorPage.FolderEditor
|
||||
else -> CollectionEditorPage.Root
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CollectionEditorScreen(
|
||||
collectionId: String?,
|
||||
onBack: () -> Unit,
|
||||
initialPage: CollectionEditorPage? = null,
|
||||
initializeRepository: Boolean = true,
|
||||
onNavigateToPage: ((page: CollectionEditorPage, title: String) -> Unit)? = null,
|
||||
) {
|
||||
val state by CollectionEditorRepository.uiState.collectAsState()
|
||||
val bottomInset = nuvioSafeBottomPadding()
|
||||
|
||||
LaunchedEffect(collectionId) {
|
||||
CollectionEditorRepository.initialize(collectionId)
|
||||
LaunchedEffect(collectionId, initializeRepository) {
|
||||
if (initializeRepository) {
|
||||
CollectionEditorRepository.initialize(collectionId)
|
||||
}
|
||||
}
|
||||
|
||||
val page = initialPage ?: state.activeEditorPage()
|
||||
val initialPickerCompletionGeneration = remember(initialPage) {
|
||||
state.sourcePickerCompletionGeneration
|
||||
}
|
||||
|
||||
LaunchedEffect(initialPage, state.sourcePickerCompletionGeneration) {
|
||||
if (
|
||||
initialPage in autoDismissedPickerPages &&
|
||||
state.sourcePickerCompletionGeneration != initialPickerCompletionGeneration
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
initialPage != null &&
|
||||
page != CollectionEditorPage.Root &&
|
||||
state.editingFolder == null
|
||||
) {
|
||||
LaunchedEffect(initialPage) { onBack() }
|
||||
return
|
||||
}
|
||||
|
||||
fun closePage(pageToClose: CollectionEditorPage, close: () -> Unit) {
|
||||
close()
|
||||
if (initialPage == pageToClose) {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
|
||||
val editingFolder = state.editingFolder
|
||||
if (state.showFolderEditor && editingFolder != null) {
|
||||
if (state.showCatalogPicker) {
|
||||
CatalogPickerScreen(
|
||||
availableCatalogs = state.availableCatalogs,
|
||||
selectedSources = editingFolder.resolvedCatalogSources,
|
||||
onToggle = { CollectionEditorRepository.toggleCatalogSource(it) },
|
||||
onBack = { CollectionEditorRepository.hideCatalogPicker() },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.showTmdbSourcePicker) {
|
||||
TmdbSourcePickerScreen(
|
||||
state = state,
|
||||
onBack = { CollectionEditorRepository.hideTmdbSourcePicker() },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.showTraktSourcePicker) {
|
||||
TraktSourcePickerScreen(
|
||||
state = state,
|
||||
onBack = { CollectionEditorRepository.hideTraktSourcePicker() },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (page == CollectionEditorPage.FolderEditor && editingFolder != null) {
|
||||
val genrePickerIndex = state.genrePickerSourceIndex
|
||||
val genrePickerSource = genrePickerIndex?.let { editingFolder.resolvedSources.getOrNull(it) }
|
||||
val genrePickerCatalogSource = genrePickerSource?.addonCatalogSource()
|
||||
|
|
@ -125,7 +166,18 @@ fun CollectionEditorScreen(
|
|||
|
||||
FolderEditorPage(
|
||||
state = state,
|
||||
onBack = { CollectionEditorRepository.cancelFolderEdit() },
|
||||
onBack = {
|
||||
closePage(CollectionEditorPage.FolderEditor) {
|
||||
CollectionEditorRepository.cancelFolderEdit()
|
||||
}
|
||||
},
|
||||
onNavigateToPage = onNavigateToPage,
|
||||
onSave = {
|
||||
CollectionEditorRepository.saveFolderEdit()
|
||||
if (initialPage == CollectionEditorPage.FolderEditor) {
|
||||
onBack()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
@ -149,28 +201,40 @@ fun CollectionEditorScreen(
|
|||
return
|
||||
}
|
||||
|
||||
if (state.showCatalogPicker) {
|
||||
if (page == CollectionEditorPage.CatalogPicker) {
|
||||
CatalogPickerScreen(
|
||||
availableCatalogs = state.availableCatalogs,
|
||||
selectedSources = state.editingFolder?.resolvedCatalogSources.orEmpty(),
|
||||
onToggle = { CollectionEditorRepository.toggleCatalogSource(it) },
|
||||
onBack = { CollectionEditorRepository.hideCatalogPicker() },
|
||||
onBack = {
|
||||
closePage(CollectionEditorPage.CatalogPicker) {
|
||||
CollectionEditorRepository.hideCatalogPicker()
|
||||
}
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.showTmdbSourcePicker) {
|
||||
if (page == CollectionEditorPage.TmdbSourcePicker) {
|
||||
TmdbSourcePickerScreen(
|
||||
state = state,
|
||||
onBack = { CollectionEditorRepository.hideTmdbSourcePicker() },
|
||||
onBack = {
|
||||
closePage(CollectionEditorPage.TmdbSourcePicker) {
|
||||
CollectionEditorRepository.hideTmdbSourcePicker()
|
||||
}
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.showTraktSourcePicker) {
|
||||
if (page == CollectionEditorPage.TraktSourcePicker) {
|
||||
TraktSourcePickerScreen(
|
||||
state = state,
|
||||
onBack = { CollectionEditorRepository.hideTraktSourcePicker() },
|
||||
onBack = {
|
||||
closePage(CollectionEditorPage.TraktSourcePicker) {
|
||||
CollectionEditorRepository.hideTraktSourcePicker()
|
||||
}
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -335,7 +399,10 @@ fun CollectionEditorScreen(
|
|||
) {
|
||||
NuvioSectionLabel(text = stringResource(Res.string.collections_editor_folders))
|
||||
TextButton(
|
||||
onClick = { CollectionEditorRepository.addFolder(newFolderTitle) },
|
||||
onClick = {
|
||||
CollectionEditorRepository.addFolder(newFolderTitle)
|
||||
onNavigateToPage?.invoke(CollectionEditorPage.FolderEditor, newFolderTitle)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
|
|
@ -351,9 +418,13 @@ fun CollectionEditorScreen(
|
|||
// Folder Items
|
||||
if (state.folders.isNotEmpty()) {
|
||||
item {
|
||||
val editFolderTitle = stringResource(Res.string.collections_editor_edit_folder)
|
||||
FolderReorderableList(
|
||||
folders = state.folders,
|
||||
onEdit = { CollectionEditorRepository.editFolder(it) },
|
||||
onEdit = {
|
||||
CollectionEditorRepository.editFolder(it)
|
||||
onNavigateToPage?.invoke(CollectionEditorPage.FolderEditor, editFolderTitle)
|
||||
},
|
||||
onDelete = { CollectionEditorRepository.removeFolder(it) },
|
||||
)
|
||||
}
|
||||
|
|
@ -558,9 +629,15 @@ private fun FolderListItem(
|
|||
private fun FolderEditorPage(
|
||||
state: CollectionEditorUiState,
|
||||
onBack: () -> Unit,
|
||||
onNavigateToPage: ((page: CollectionEditorPage, title: String) -> Unit)?,
|
||||
onSave: () -> Unit,
|
||||
) {
|
||||
val folder = state.editingFolder ?: return
|
||||
val bottomInset = nuvioSafeBottomPadding()
|
||||
val catalogPickerTitle = stringResource(Res.string.collections_editor_select_catalogs)
|
||||
val tmdbSourcePickerTitle = stringResource(Res.string.collections_editor_tmdb_sources)
|
||||
val traktSourcePickerTitle = stringResource(Res.string.collections_editor_trakt_sources)
|
||||
val editTraktSourcePickerTitle = stringResource(Res.string.collections_editor_edit_trakt_source)
|
||||
|
||||
PlatformBackHandler(enabled = true) {
|
||||
onBack()
|
||||
|
|
@ -725,7 +802,15 @@ private fun FolderEditorPage(
|
|||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
TextButton(onClick = { CollectionEditorRepository.showTmdbSourcePicker() }) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
CollectionEditorRepository.showTmdbSourcePicker()
|
||||
onNavigateToPage?.invoke(
|
||||
CollectionEditorPage.TmdbSourcePicker,
|
||||
tmdbSourcePickerTitle,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = null,
|
||||
|
|
@ -734,7 +819,15 @@ private fun FolderEditorPage(
|
|||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(stringResource(Res.string.source_tmdb))
|
||||
}
|
||||
TextButton(onClick = { CollectionEditorRepository.showTraktSourcePicker() }) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
CollectionEditorRepository.showTraktSourcePicker()
|
||||
onNavigateToPage?.invoke(
|
||||
CollectionEditorPage.TraktSourcePicker,
|
||||
traktSourcePickerTitle,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = null,
|
||||
|
|
@ -743,7 +836,15 @@ private fun FolderEditorPage(
|
|||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(stringResource(Res.string.collections_editor_add_trakt_source))
|
||||
}
|
||||
TextButton(onClick = { CollectionEditorRepository.showCatalogPicker() }) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
CollectionEditorRepository.showCatalogPicker()
|
||||
onNavigateToPage?.invoke(
|
||||
CollectionEditorPage.CatalogPicker,
|
||||
catalogPickerTitle,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = null,
|
||||
|
|
@ -784,7 +885,13 @@ private fun FolderEditorPage(
|
|||
} else if (source.isTrakt) {
|
||||
FolderTraktSourceCard(
|
||||
source = source,
|
||||
onEdit = { CollectionEditorRepository.editTraktSource(index) },
|
||||
onEdit = {
|
||||
CollectionEditorRepository.editTraktSource(index)
|
||||
onNavigateToPage?.invoke(
|
||||
CollectionEditorPage.TraktSourcePicker,
|
||||
editTraktSourcePickerTitle,
|
||||
)
|
||||
},
|
||||
onRemove = { CollectionEditorRepository.removeCatalogSource(index) },
|
||||
)
|
||||
} else if (addonSource != null) {
|
||||
|
|
@ -823,7 +930,7 @@ private fun FolderEditorPage(
|
|||
NuvioPrimaryButton(
|
||||
text = stringResource(Res.string.collections_editor_save),
|
||||
enabled = folder.title.isNotBlank(),
|
||||
onClick = { CollectionEditorRepository.saveFolderEdit() },
|
||||
onClick = onSave,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
|||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import com.nuvio.app.navigation.LocalUseNativeNavigation
|
||||
|
||||
private sealed interface PersonDetailUiState {
|
||||
data object Loading : PersonDetailUiState
|
||||
|
|
@ -146,19 +147,20 @@ fun PersonDetailScreen(
|
|||
)
|
||||
}
|
||||
|
||||
// Back button overlaid on top
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.statusBars)
|
||||
.padding(start = 4.dp, top = 4.dp)
|
||||
.align(Alignment.TopStart),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(Res.string.action_back),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
if (!LocalUseNativeNavigation.current) {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.statusBars)
|
||||
.padding(start = 4.dp, top = 4.dp)
|
||||
.align(Alignment.TopStart),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(Res.string.action_back),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import com.nuvio.app.features.tmdb.TmdbEntityMediaType
|
|||
import com.nuvio.app.features.tmdb.TmdbEntityRailType
|
||||
import com.nuvio.app.features.tmdb.TmdbMetadataService
|
||||
import com.nuvio.app.features.watched.WatchedRepository
|
||||
import com.nuvio.app.navigation.LocalUseNativeNavigation
|
||||
|
||||
private sealed interface EntityBrowseUiState {
|
||||
data object Loading : EntityBrowseUiState
|
||||
|
|
@ -126,18 +127,20 @@ fun TmdbEntityBrowseScreen(
|
|||
}
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.statusBars)
|
||||
.padding(start = 4.dp, top = 4.dp)
|
||||
.align(Alignment.TopStart),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(Res.string.action_back),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
if (!LocalUseNativeNavigation.current) {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.statusBars)
|
||||
.padding(start = 4.dp, top = 4.dp)
|
||||
.align(Alignment.TopStart),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(Res.string.action_back),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,10 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.lerp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.core.ui.NuvioBackButton
|
||||
import com.nuvio.app.core.ui.platformPhysicalTopInset
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
import com.nuvio.app.isIos
|
||||
import com.nuvio.app.navigation.LocalUseNativeNavigation
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
|
|
@ -54,7 +56,12 @@ fun DetailFloatingHeader(
|
|||
onToggleSaved: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val safeAreaTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
val useNativeNavigation = LocalUseNativeNavigation.current
|
||||
val safeAreaTop = if (useNativeNavigation) {
|
||||
platformPhysicalTopInset()
|
||||
} else {
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
}
|
||||
val headerTopPadding = (safeAreaTop - 6.dp).coerceAtLeast(safeAreaTop * 0.8f)
|
||||
val interactive = progress > 0.05f
|
||||
val surfaceColor = backgroundColor ?: if (isIos) {
|
||||
|
|
@ -93,7 +100,7 @@ fun DetailFloatingHeader(
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (interactive) {
|
||||
if (interactive && !useNativeNavigation) {
|
||||
NuvioBackButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.size(40.dp),
|
||||
|
|
@ -103,6 +110,9 @@ fun DetailFloatingHeader(
|
|||
iconSize = 24.dp,
|
||||
)
|
||||
} else {
|
||||
// Native iOS navigation owns the back button, but retaining
|
||||
// this slot keeps the Compose logo centered as the floating
|
||||
// header replaces the hero while scrolling.
|
||||
Box(modifier = Modifier.size(40.dp))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,13 +47,16 @@ import org.jetbrains.compose.resources.stringResource
|
|||
fun DownloadsScreen(
|
||||
onBack: () -> Unit,
|
||||
onOpenDownload: (DownloadItem) -> Unit,
|
||||
initialShowId: String? = null,
|
||||
onNavigateToShow: ((showId: String, title: String) -> Unit)? = null,
|
||||
onBackFromShow: (() -> Unit)? = null,
|
||||
) {
|
||||
val uiState by remember {
|
||||
DownloadsRepository.ensureLoaded()
|
||||
DownloadsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
||||
var selectedShowId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var selectedShowId by rememberSaveable(initialShowId) { mutableStateOf(initialShowId) }
|
||||
val openDownloadsDirectoryFailedText = stringResource(Res.string.downloads_open_directory_failed)
|
||||
|
||||
val completedEpisodes = remember(uiState.items) {
|
||||
|
|
@ -78,7 +81,7 @@ fun DownloadsScreen(
|
|||
},
|
||||
onBack = {
|
||||
if (selectedShowId != null) {
|
||||
selectedShowId = null
|
||||
onBackFromShow?.invoke() ?: run { selectedShowId = null }
|
||||
} else {
|
||||
onBack()
|
||||
}
|
||||
|
|
@ -104,7 +107,9 @@ fun DownloadsScreen(
|
|||
downloadsRootContent(
|
||||
uiState = uiState,
|
||||
onOpenDownload = onOpenDownload,
|
||||
onOpenShow = { showId -> selectedShowId = showId },
|
||||
onOpenShow = { showId, title ->
|
||||
onNavigateToShow?.invoke(showId, title) ?: run { selectedShowId = showId }
|
||||
},
|
||||
)
|
||||
} else {
|
||||
downloadsShowContent(
|
||||
|
|
@ -119,7 +124,7 @@ fun DownloadsScreen(
|
|||
private fun LazyListScope.downloadsRootContent(
|
||||
uiState: DownloadsUiState,
|
||||
onOpenDownload: (DownloadItem) -> Unit,
|
||||
onOpenShow: (String) -> Unit,
|
||||
onOpenShow: (showId: String, title: String) -> Unit,
|
||||
) {
|
||||
val activeItems = uiState.activeItems
|
||||
val completedMovies = uiState.completedItems.filterNot(DownloadItem::isEpisode)
|
||||
|
|
@ -183,7 +188,7 @@ private fun LazyListScope.downloadsRootContent(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
.clickable { onOpenShow(item.parentMetaId) },
|
||||
.clickable { onOpenShow(item.parentMetaId, item.title) },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.serialization.Serializable
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.player_ios_hardware_decoder_off
|
||||
import nuvio.composeapp.generated.resources.player_ios_preset_compatibility_desc
|
||||
|
|
@ -14,11 +13,6 @@ import nuvio.composeapp.generated.resources.player_ios_preset_sdr_tone_mapped_de
|
|||
import nuvio.composeapp.generated.resources.player_ios_preset_sdr_tone_mapped_label
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Serializable
|
||||
data class PlayerRoute(
|
||||
val launchId: Long,
|
||||
)
|
||||
|
||||
data class PlayerLaunch(
|
||||
val profileId: Int,
|
||||
val title: String,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import com.nuvio.app.features.tmdb.TmdbSettings
|
|||
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesUiState
|
||||
import com.nuvio.app.navigation.LocalUseNativeNavigation
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.compose_settings_page_root
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -99,13 +100,26 @@ private fun SettingsPage.isEnabledByPolicy(): Boolean =
|
|||
else -> true
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun settingsPageTitles(): Map<SettingsPage, String> {
|
||||
val titles = mutableMapOf<SettingsPage, String>()
|
||||
for (page in SettingsPage.entries) {
|
||||
titles[page] = stringResource(page.titleRes)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
rootActionRequests: Flow<Unit> = emptyFlow(),
|
||||
initialPageName: String = SettingsPage.Root.name,
|
||||
requestedPageName: String? = null,
|
||||
onRequestedPageConsumed: () -> Unit = {},
|
||||
rootActionsEnabled: Boolean = true,
|
||||
onNavigatePage: ((pageName: String, title: String) -> Unit)? = null,
|
||||
onExternalBack: (() -> Unit)? = null,
|
||||
showInternalHeader: Boolean = true,
|
||||
onSwitchProfile: (() -> Unit)? = null,
|
||||
onHomescreenClick: () -> Unit = {},
|
||||
onMetaScreenClick: () -> Unit = {},
|
||||
|
|
@ -135,7 +149,10 @@ fun SettingsScreen(
|
|||
val liquidGlassNativeTabBarEnabled by remember {
|
||||
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled
|
||||
}.collectAsStateWithLifecycle()
|
||||
val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() }
|
||||
val useNativeNavigation = LocalUseNativeNavigation.current
|
||||
val liquidGlassNativeTabBarSupported = remember(useNativeNavigation) {
|
||||
!useNativeNavigation && isLiquidGlassNativeTabBarSupported()
|
||||
}
|
||||
val selectedAppLanguage by remember { ThemeSettingsRepository.selectedAppLanguage }.collectAsStateWithLifecycle()
|
||||
val tmdbSettings by remember {
|
||||
TmdbSettingsRepository.ensureLoaded()
|
||||
|
|
@ -219,8 +236,15 @@ fun SettingsScreen(
|
|||
HomeCatalogSettingsRepository.syncCollections(collections)
|
||||
}
|
||||
|
||||
var currentPage by rememberSaveable { mutableStateOf(SettingsPage.Root.name) }
|
||||
val initialPage = remember(initialPageName) {
|
||||
runCatching { SettingsPage.valueOf(initialPageName) }
|
||||
.getOrDefault(SettingsPage.Root)
|
||||
.takeIf { it.isEnabledByPolicy() }
|
||||
?: SettingsPage.Root
|
||||
}
|
||||
var currentPage by rememberSaveable(initialPageName) { mutableStateOf(initialPage.name) }
|
||||
val scrollToTopRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val pageTitles = settingsPageTitles()
|
||||
val page = remember(currentPage) {
|
||||
runCatching { SettingsPage.valueOf(currentPage) }
|
||||
.getOrDefault(SettingsPage.Root)
|
||||
|
|
@ -229,6 +253,73 @@ fun SettingsScreen(
|
|||
}
|
||||
val previousPage = page.previousPage()
|
||||
|
||||
fun openPage(targetPage: SettingsPage) {
|
||||
if (!targetPage.isEnabledByPolicy()) return
|
||||
val externalNavigator = onNavigatePage
|
||||
if (externalNavigator == null) {
|
||||
currentPage = targetPage.name
|
||||
return
|
||||
}
|
||||
if (targetPage == SettingsPage.Root && onExternalBack != null) {
|
||||
onExternalBack()
|
||||
return
|
||||
}
|
||||
externalNavigator(
|
||||
targetPage.name,
|
||||
pageTitles.getValue(targetPage),
|
||||
)
|
||||
}
|
||||
|
||||
fun navigateBack() {
|
||||
val parentPage = previousPage ?: return
|
||||
if (onNavigatePage != null && onExternalBack != null) {
|
||||
onExternalBack()
|
||||
} else {
|
||||
currentPage = parentPage.name
|
||||
}
|
||||
}
|
||||
|
||||
val openHomescreen = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.Homescreen) }
|
||||
} else {
|
||||
onHomescreenClick
|
||||
}
|
||||
val openMetaScreen = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.MetaScreen) }
|
||||
} else {
|
||||
onMetaScreenClick
|
||||
}
|
||||
val openContinueWatching = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.ContinueWatching) }
|
||||
} else {
|
||||
onContinueWatchingClick
|
||||
}
|
||||
val openAddons = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.Addons) }
|
||||
} else {
|
||||
onAddonsClick
|
||||
}
|
||||
val openPlugins = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.Plugins) }
|
||||
} else {
|
||||
onPluginsClick
|
||||
}
|
||||
val openAccount = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.Account) }
|
||||
} else {
|
||||
onAccountClick
|
||||
}
|
||||
val openSupportersContributors = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.SupportersContributors) }
|
||||
} else {
|
||||
onSupportersContributorsClick
|
||||
}
|
||||
val openLicensesAttributions = if (onNavigatePage != null) {
|
||||
{ openPage(SettingsPage.LicensesAttributions) }
|
||||
} else {
|
||||
onLicensesAttributionsClick
|
||||
}
|
||||
|
||||
LaunchedEffect(page, currentPage) {
|
||||
if (page.name != currentPage) {
|
||||
currentPage = page.name
|
||||
|
|
@ -240,7 +331,7 @@ fun SettingsScreen(
|
|||
if (!rootActionsEnabled) return@collect
|
||||
val pageToOpen = page.previousPage()
|
||||
if (pageToOpen != null) {
|
||||
currentPage = pageToOpen.name
|
||||
navigateBack()
|
||||
} else {
|
||||
scrollToTopRequests.tryEmit(Unit)
|
||||
}
|
||||
|
|
@ -255,20 +346,22 @@ fun SettingsScreen(
|
|||
return@LaunchedEffect
|
||||
}
|
||||
if (!rootActionsEnabled) return@LaunchedEffect
|
||||
currentPage = targetPage.name
|
||||
openPage(targetPage)
|
||||
onRequestedPageConsumed()
|
||||
}
|
||||
|
||||
PlatformBackHandler(
|
||||
enabled = rootActionsEnabled && previousPage != null,
|
||||
onBack = { previousPage?.let { currentPage = it.name } },
|
||||
enabled = previousPage != null && (rootActionsEnabled || onExternalBack != null),
|
||||
onBack = ::navigateBack,
|
||||
)
|
||||
|
||||
if (maxWidth >= 768.dp) {
|
||||
TabletSettingsScreen(
|
||||
page = page,
|
||||
scrollToTopRequests = scrollToTopRequests,
|
||||
onPageChange = { currentPage = it.name },
|
||||
onPageChange = ::openPage,
|
||||
onNavigateBack = ::navigateBack,
|
||||
showInternalHeader = showInternalHeader,
|
||||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
holdToSpeedEnabled = playerSettingsUiState.holdToSpeedEnabled,
|
||||
holdToSpeedValue = playerSettingsUiState.holdToSpeedValue,
|
||||
|
|
@ -314,8 +407,8 @@ fun SettingsScreen(
|
|||
posterCardStyleUiState = posterCardStyleUiState,
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onSupportersContributorsClick = onSupportersContributorsClick,
|
||||
onLicensesAttributionsClick = onLicensesAttributionsClick,
|
||||
onSupportersContributorsClick = openSupportersContributors,
|
||||
onLicensesAttributionsClick = openLicensesAttributions,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
|
|
@ -323,7 +416,9 @@ fun SettingsScreen(
|
|||
MobileSettingsScreen(
|
||||
page = page,
|
||||
scrollToTopRequests = scrollToTopRequests,
|
||||
onPageChange = { currentPage = it.name },
|
||||
onPageChange = ::openPage,
|
||||
onNavigateBack = ::navigateBack,
|
||||
showInternalHeader = showInternalHeader,
|
||||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
holdToSpeedEnabled = playerSettingsUiState.holdToSpeedEnabled,
|
||||
holdToSpeedValue = playerSettingsUiState.holdToSpeedValue,
|
||||
|
|
@ -368,15 +463,15 @@ fun SettingsScreen(
|
|||
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
||||
posterCardStyleUiState = posterCardStyleUiState,
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
onHomescreenClick = onHomescreenClick,
|
||||
onMetaScreenClick = onMetaScreenClick,
|
||||
onContinueWatchingClick = onContinueWatchingClick,
|
||||
onAddonsClick = onAddonsClick,
|
||||
onPluginsClick = onPluginsClick,
|
||||
onHomescreenClick = openHomescreen,
|
||||
onMetaScreenClick = openMetaScreen,
|
||||
onContinueWatchingClick = openContinueWatching,
|
||||
onAddonsClick = openAddons,
|
||||
onPluginsClick = openPlugins,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onAccountClick = onAccountClick,
|
||||
onSupportersContributorsClick = onSupportersContributorsClick,
|
||||
onLicensesAttributionsClick = onLicensesAttributionsClick,
|
||||
onAccountClick = openAccount,
|
||||
onSupportersContributorsClick = openSupportersContributors,
|
||||
onLicensesAttributionsClick = openLicensesAttributions,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
|
|
@ -389,6 +484,8 @@ private fun MobileSettingsScreen(
|
|||
page: SettingsPage,
|
||||
scrollToTopRequests: Flow<Unit>,
|
||||
onPageChange: (SettingsPage) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
showInternalHeader: Boolean,
|
||||
showLoadingOverlay: Boolean,
|
||||
holdToSpeedEnabled: Boolean,
|
||||
holdToSpeedValue: Float,
|
||||
|
|
@ -521,12 +618,16 @@ private fun MobileSettingsScreen(
|
|||
modifier = Modifier.nestedScroll(rootSearchRevealConnection),
|
||||
listState = listState,
|
||||
) {
|
||||
stickyHeader {
|
||||
val previousPage = page.previousPage()
|
||||
NuvioScreenHeader(
|
||||
title = stringResource(page.titleRes),
|
||||
onBack = previousPage?.let { { onPageChange(it) } },
|
||||
)
|
||||
if (showInternalHeader) {
|
||||
stickyHeader {
|
||||
val previousPage = page.previousPage()
|
||||
NuvioScreenHeader(
|
||||
title = stringResource(page.titleRes),
|
||||
onBack = previousPage?.let { { onNavigateBack() } },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item { Spacer(modifier = Modifier.height(44.dp)) }
|
||||
}
|
||||
|
||||
when (page) {
|
||||
|
|
@ -733,6 +834,8 @@ private fun TabletSettingsScreen(
|
|||
page: SettingsPage,
|
||||
scrollToTopRequests: Flow<Unit>,
|
||||
onPageChange: (SettingsPage) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
showInternalHeader: Boolean,
|
||||
showLoadingOverlay: Boolean,
|
||||
holdToSpeedEnabled: Boolean,
|
||||
holdToSpeedValue: Float,
|
||||
|
|
@ -911,21 +1014,23 @@ private fun TabletSettingsScreen(
|
|||
),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||
) {
|
||||
item {
|
||||
val previousPage = page.previousPage()
|
||||
TabletPageHeader(
|
||||
title = if (page == SettingsPage.Root) {
|
||||
if (settingsSearchQuery.isBlank()) {
|
||||
stringResource(activeCategory.labelRes)
|
||||
if (showInternalHeader) {
|
||||
item {
|
||||
val previousPage = page.previousPage()
|
||||
TabletPageHeader(
|
||||
title = if (page == SettingsPage.Root) {
|
||||
if (settingsSearchQuery.isBlank()) {
|
||||
stringResource(activeCategory.labelRes)
|
||||
} else {
|
||||
stringResource(Res.string.compose_settings_page_root)
|
||||
}
|
||||
} else {
|
||||
stringResource(Res.string.compose_settings_page_root)
|
||||
}
|
||||
} else {
|
||||
stringResource(page.titleRes)
|
||||
},
|
||||
showBack = previousPage != null,
|
||||
onBack = { previousPage?.let(onPageChange) },
|
||||
)
|
||||
stringResource(page.titleRes)
|
||||
},
|
||||
showBack = previousPage != null,
|
||||
onBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
when (page) {
|
||||
SettingsPage.Root -> {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.OpenInNew
|
||||
|
|
@ -61,6 +60,7 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -90,6 +90,7 @@ import com.nuvio.app.features.debrid.DirectDebridPlaybackResolver
|
|||
import com.nuvio.app.features.debrid.toastMessage
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||
import com.nuvio.app.navigation.LocalUseNativeNavigation
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
|
|
@ -127,6 +128,7 @@ fun StreamsScreen(
|
|||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val useNativeNavigation = LocalUseNativeNavigation.current
|
||||
val uiState by StreamsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val playerSettings by remember {
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
|
|
@ -205,6 +207,16 @@ fun StreamsScreen(
|
|||
} else {
|
||||
background ?: poster
|
||||
}
|
||||
val reloadStreams: () -> Unit = {
|
||||
StreamsRepository.reload(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = seasonNumber,
|
||||
episode = episodeNumber,
|
||||
manualSelection = manualSelection,
|
||||
)
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
|
|
@ -233,6 +245,7 @@ fun StreamsScreen(
|
|||
onStreamSelected(stream, positionMs, progressFraction)
|
||||
},
|
||||
onStreamLongPress = { stream -> streamActionsTarget = stream },
|
||||
onRefresh = reloadStreams,
|
||||
)
|
||||
} else {
|
||||
MobileStreamsLayout(
|
||||
|
|
@ -252,6 +265,7 @@ fun StreamsScreen(
|
|||
onStreamSelected(stream, positionMs, progressFraction)
|
||||
},
|
||||
onStreamLongPress = { stream -> streamActionsTarget = stream },
|
||||
onRefresh = reloadStreams,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -259,7 +273,7 @@ fun StreamsScreen(
|
|||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top))
|
||||
.padding(start = 12.dp, top = 8.dp),
|
||||
.padding(start = 12.dp, top = if (useNativeNavigation) 52.dp else 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
NuvioBackButton(
|
||||
|
|
@ -269,35 +283,6 @@ fun StreamsScreen(
|
|||
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.45f),
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.background.copy(alpha = 0.45f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickable(
|
||||
onClick = {
|
||||
StreamsRepository.reload(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = seasonNumber,
|
||||
episode = episodeNumber,
|
||||
manualSelection = manualSelection,
|
||||
)
|
||||
},
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Refresh,
|
||||
contentDescription = stringResource(Res.string.streams_refresh),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
|
|
@ -470,6 +455,7 @@ private fun MobileStreamsLayout(
|
|||
resumeProgressFraction: Float?,
|
||||
onStreamSelected: (stream: StreamItem, resumePositionMs: Long?, resumeProgressFraction: Float?) -> Unit,
|
||||
onStreamLongPress: (StreamItem) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
|
|
@ -542,6 +528,7 @@ private fun MobileStreamsLayout(
|
|||
groups = uiState.groups,
|
||||
selectedFilter = uiState.selectedFilter,
|
||||
onFilterSelected = { addonId -> StreamsRepository.selectFilter(addonId) },
|
||||
onRefresh = onRefresh,
|
||||
)
|
||||
|
||||
StreamList(
|
||||
|
|
@ -751,10 +738,10 @@ internal fun ProviderFilterRow(
|
|||
groups: List<AddonStreamGroup>,
|
||||
selectedFilter: String?,
|
||||
onFilterSelected: (String?) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val addonGroups = groups.filter { it.streams.isNotEmpty() || it.isLoading }
|
||||
if (addonGroups.isEmpty()) return
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
|
|
@ -763,6 +750,12 @@ internal fun ProviderFilterRow(
|
|||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
icon = Icons.Rounded.Refresh,
|
||||
contentDescription = stringResource(Res.string.streams_refresh),
|
||||
isSelected = false,
|
||||
onClick = onRefresh,
|
||||
)
|
||||
// "All" chip
|
||||
FilterChip(
|
||||
label = stringResource(Res.string.collections_tab_all),
|
||||
|
|
@ -781,7 +774,9 @@ internal fun ProviderFilterRow(
|
|||
|
||||
@Composable
|
||||
private fun FilterChip(
|
||||
label: String,
|
||||
label: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
contentDescription: String? = null,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
|
|
@ -816,6 +811,7 @@ private fun FilterChip(
|
|||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
.height(36.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(containerColor)
|
||||
.clickable(
|
||||
|
|
@ -823,18 +819,34 @@ private fun FilterChip(
|
|||
indication = null,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
.padding(horizontal = 14.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium.copy(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.SemiBold,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
color = contentColor,
|
||||
maxLines = 1,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium.copy(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.SemiBold,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
color = contentColor,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ internal fun TabletStreamsLayout(
|
|||
resumeProgressFraction: Float?,
|
||||
onStreamSelected: (stream: StreamItem, resumePositionMs: Long?, resumeProgressFraction: Float?) -> Unit,
|
||||
onStreamLongPress: (StreamItem) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hazeState = rememberHazeState()
|
||||
|
|
@ -196,6 +197,7 @@ internal fun TabletStreamsLayout(
|
|||
groups = uiState.groups,
|
||||
selectedFilter = uiState.selectedFilter,
|
||||
onFilterSelected = { addonId -> StreamsRepository.selectFilter(addonId) },
|
||||
onRefresh = onRefresh,
|
||||
)
|
||||
|
||||
ActiveScrapersStatusBlock(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.nuvio.app.navigation
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
/** True when SwiftUI owns the current iOS tab and navigation stack. */
|
||||
val LocalUseNativeNavigation = staticCompositionLocalOf { false }
|
||||
|
||||
/** True for immersive routes that intentionally keep their Compose-owned exit controls. */
|
||||
val LocalNativeNavigationBarHidden = staticCompositionLocalOf { false }
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.nuvio.app.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
internal class NuvioNavigator(
|
||||
private val backStack: NavBackStack<NavKey>,
|
||||
private val onExternalNavigate: ((AppRoute, launchSingleTop: Boolean) -> Unit)? = null,
|
||||
private val onExternalBack: (() -> Unit)? = null,
|
||||
private val onExternalReplace: ((AppRoute) -> Unit)? = null,
|
||||
private val onRouteRemoved: (AppRoute) -> Unit = {},
|
||||
) {
|
||||
val currentRoute: AppRoute?
|
||||
get() = backStack.lastOrNull() as? AppRoute
|
||||
|
||||
val routes: List<AppRoute>
|
||||
get() = backStack.filterIsInstance<AppRoute>()
|
||||
|
||||
fun navigate(route: AppRoute, options: NuvioNavigateOptions.() -> Unit = {}) {
|
||||
val resolvedOptions = NuvioNavigateOptions().apply(options)
|
||||
|
||||
if (resolvedOptions.launchSingleTop && currentRoute == route) return
|
||||
|
||||
val popUpToRoute = resolvedOptions.popUpToRoute
|
||||
if (popUpToRoute != null) {
|
||||
val targetIndex = backStack.indexOfLast { popUpToRoute.isInstance(it) }
|
||||
if (targetIndex >= 0) {
|
||||
if (
|
||||
onExternalReplace != null &&
|
||||
resolvedOptions.popUpToInclusive &&
|
||||
targetIndex == backStack.lastIndex
|
||||
) {
|
||||
onExternalReplace(route)
|
||||
return
|
||||
}
|
||||
|
||||
val firstRemovedIndex = if (resolvedOptions.popUpToInclusive) targetIndex else targetIndex + 1
|
||||
if (firstRemovedIndex <= backStack.lastIndex) {
|
||||
val removedRoutes = backStack
|
||||
.subList(firstRemovedIndex, backStack.size)
|
||||
.filterIsInstance<AppRoute>()
|
||||
.toList()
|
||||
backStack.subList(firstRemovedIndex, backStack.size).clear()
|
||||
removedRoutes.forEach(onRouteRemoved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedOptions.launchSingleTop && currentRoute == route) return
|
||||
onExternalNavigate?.invoke(route, resolvedOptions.launchSingleTop) ?: backStack.add(route)
|
||||
}
|
||||
|
||||
fun popBackStack(expectedRoute: AppRoute? = null): Boolean {
|
||||
if (expectedRoute != null && currentRoute != expectedRoute) return false
|
||||
if (backStack.size > 1) {
|
||||
(backStack.removeAt(backStack.lastIndex) as? AppRoute)?.let(onRouteRemoved)
|
||||
return true
|
||||
}
|
||||
onExternalBack?.invoke()
|
||||
return onExternalBack != null
|
||||
}
|
||||
}
|
||||
|
||||
internal class NuvioNavigateOptions {
|
||||
var launchSingleTop: Boolean = false
|
||||
internal var popUpToRoute: KClass<out AppRoute>? = null
|
||||
internal var popUpToInclusive: Boolean = false
|
||||
|
||||
inline fun <reified T : AppRoute> popUpTo(noinline options: NuvioPopUpToOptions.() -> Unit = {}) {
|
||||
val resolved = NuvioPopUpToOptions().apply(options)
|
||||
popUpToRoute = T::class
|
||||
popUpToInclusive = resolved.inclusive
|
||||
}
|
||||
}
|
||||
|
||||
internal class NuvioPopUpToOptions {
|
||||
var inclusive: Boolean = false
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package com.nuvio.app.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface AppRoute : NavKey {
|
||||
val title: String?
|
||||
get() = null
|
||||
|
||||
val subtitle: String?
|
||||
get() = null
|
||||
|
||||
/** Full-screen destinations such as the video player keep native navigation chrome hidden. */
|
||||
val hidesNavigationBar: Boolean
|
||||
get() = false
|
||||
|
||||
/** Stable enough to apply Navigation 3 launchSingleTop semantics in SwiftUI. */
|
||||
val navigationIdentity: String
|
||||
get() = toString()
|
||||
|
||||
/** Lets an explicitly cross-tab route select its native SwiftUI stack. */
|
||||
val preferredTabName: String?
|
||||
get() = null
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed interface SettingsDestinationRoute : AppRoute {
|
||||
override val preferredTabName: String
|
||||
get() = "Settings"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object TabsRoute : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class DetailRoute(
|
||||
val type: String,
|
||||
val id: String,
|
||||
override val title: String? = null,
|
||||
) : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class PersonDetailRoute(
|
||||
val personId: Int,
|
||||
val personName: String,
|
||||
val personPhoto: String? = null,
|
||||
val castAvatarTransitionKey: String? = null,
|
||||
val preferCrew: Boolean = false,
|
||||
) : AppRoute {
|
||||
override val title: String
|
||||
get() = personName
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EntityBrowseRoute(
|
||||
val entityKind: String,
|
||||
val entityId: Int,
|
||||
val entityName: String,
|
||||
val sourceType: String = "tv",
|
||||
) : AppRoute {
|
||||
override val title: String
|
||||
get() = entityName
|
||||
}
|
||||
|
||||
/** A settings leaf promoted from the former in-screen page state machine. */
|
||||
@Serializable
|
||||
data class SettingsPageRoute(
|
||||
val pageName: String,
|
||||
override val title: String,
|
||||
) : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class HomescreenSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class MetaScreenSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class ContinueWatchingSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class DownloadsSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class DownloadShowRoute(
|
||||
val showId: String,
|
||||
override val title: String,
|
||||
) : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class AddonsSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class PluginsSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class AccountSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class SupportersContributorsSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class LicensesAttributionsSettingsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class CollectionsRoute(override val title: String = "") : SettingsDestinationRoute
|
||||
|
||||
@Serializable
|
||||
data class CollectionEditorRoute(
|
||||
val collectionId: String? = null,
|
||||
override val title: String = "",
|
||||
) : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class CollectionEditorPageRoute(
|
||||
val collectionId: String? = null,
|
||||
val pageName: String,
|
||||
override val title: String,
|
||||
) : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class FolderDetailRoute(
|
||||
val collectionId: String,
|
||||
val folderId: String,
|
||||
override val title: String = "",
|
||||
) : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class StreamRoute(
|
||||
val launchId: Long,
|
||||
override val title: String = "",
|
||||
) : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class CatalogRoute(
|
||||
val launchId: Long,
|
||||
override val title: String = "",
|
||||
override val subtitle: String? = null,
|
||||
) : AppRoute
|
||||
|
||||
@Serializable
|
||||
data class PlayerRoute(
|
||||
val launchId: Long,
|
||||
override val title: String = "",
|
||||
) : AppRoute {
|
||||
override val hidesNavigationBar: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
|
@ -1,12 +1,77 @@
|
|||
package com.nuvio.app
|
||||
|
||||
import androidx.compose.ui.uikit.OnFocusBehavior
|
||||
import androidx.compose.ui.window.ComposeUIViewController
|
||||
import com.nuvio.app.core.ui.NativeProfileSwitcherController
|
||||
import com.nuvio.app.navigation.AppRoute
|
||||
import platform.UIKit.UIColor
|
||||
import platform.UIKit.UIViewController
|
||||
|
||||
private val nuvioBackgroundColor = UIColor(red = 0.051, green = 0.051, blue = 0.051, alpha = 1.0)
|
||||
|
||||
fun MainViewController() = ComposeUIViewController {
|
||||
@Suppress("unused")
|
||||
fun MainViewController(): UIViewController = nuvioComposeViewController {
|
||||
App()
|
||||
}.apply {
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun MainViewController(
|
||||
initialTabName: String,
|
||||
useNativeTabBar: Boolean,
|
||||
useTabletFloatingTabBar: Boolean,
|
||||
onNavigate: (AppRoute, Boolean) -> Unit,
|
||||
onGoBack: () -> Unit,
|
||||
onReplace: (AppRoute) -> Unit,
|
||||
onActivate: (String) -> Unit,
|
||||
onAppReady: (Boolean) -> Unit,
|
||||
onTabTitles: (String, String, String, String) -> Unit,
|
||||
nativeProfileSwitcherController: NativeProfileSwitcherController,
|
||||
): UIViewController {
|
||||
val initialTab = AppScreenTab.fromName(initialTabName)
|
||||
return nuvioComposeViewController {
|
||||
App(
|
||||
initialTab = initialTab,
|
||||
useNativeNavigation = true,
|
||||
useNativeTabBar = useNativeTabBar,
|
||||
useTabletFloatingTabBar = useTabletFloatingTabBar,
|
||||
ownsAppRuntime = initialTab == AppScreenTab.Home,
|
||||
bypassAppGate = initialTab != AppScreenTab.Home,
|
||||
onNavigate = onNavigate,
|
||||
onGoBack = onGoBack,
|
||||
onReplace = onReplace,
|
||||
onActivate = { tab -> onActivate(tab.name) },
|
||||
onAppReady = onAppReady,
|
||||
onTabTitles = onTabTitles,
|
||||
nativeProfileSwitcherController = nativeProfileSwitcherController,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun ScreenViewController(
|
||||
route: AppRoute,
|
||||
onNavigate: (AppRoute, Boolean) -> Unit,
|
||||
onGoBack: () -> Unit,
|
||||
onReplace: (AppRoute) -> Unit,
|
||||
onActivate: (String) -> Unit,
|
||||
): UIViewController = nuvioComposeViewController {
|
||||
App(
|
||||
initialRoute = route,
|
||||
useNativeNavigation = true,
|
||||
ownsAppRuntime = false,
|
||||
bypassAppGate = true,
|
||||
onNavigate = onNavigate,
|
||||
onGoBack = onGoBack,
|
||||
onReplace = onReplace,
|
||||
onActivate = { tab -> onActivate(tab.name) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun nuvioComposeViewController(
|
||||
content: @androidx.compose.runtime.Composable () -> Unit,
|
||||
): UIViewController = ComposeUIViewController(
|
||||
configure = { onFocusBehavior = OnFocusBehavior.DoNothing },
|
||||
content = content,
|
||||
).apply {
|
||||
view.backgroundColor = nuvioBackgroundColor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@ package com.nuvio.app.core.ui
|
|||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.uikit.LocalUIViewController
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.useContents
|
||||
|
||||
internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp
|
||||
internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
|
||||
|
|
@ -15,3 +20,14 @@ internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 0.dp
|
|||
@Composable
|
||||
internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets =
|
||||
WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
@Composable
|
||||
internal actual fun platformPhysicalTopInset(): Dp {
|
||||
val physicalTop = LocalUIViewController.current.view.window
|
||||
?.safeAreaInsets
|
||||
?.useContents { top.toFloat() }
|
||||
|
||||
return physicalTop?.dp
|
||||
?: WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ android-compileSdkMinor = "0"
|
|||
android-minSdk = "24"
|
||||
android-targetSdk = "36"
|
||||
androidx-activity = "1.12.2"
|
||||
androidx-navigation = "2.9.2"
|
||||
androidx-navigation3 = "1.1.1"
|
||||
androidx-appcompat = "1.7.1"
|
||||
androidx-core = "1.17.0"
|
||||
androidx-core-splashscreen = "1.0.1"
|
||||
|
|
@ -44,7 +44,7 @@ androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "an
|
|||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||
androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" }
|
||||
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
||||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||
|
|
|
|||
16
iosApp/iosApp/Assets.xcassets/NuvioTabHome.imageset/Contents.json
vendored
Normal file
16
iosApp/iosApp/Assets.xcassets/NuvioTabHome.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nuvio-tab-home.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
3
iosApp/iosApp/Assets.xcassets/NuvioTabHome.imageset/nuvio-tab-home.svg
vendored
Normal file
3
iosApp/iosApp/Assets.xcassets/NuvioTabHome.imageset/nuvio-tab-home.svg
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#000" d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 153 B |
16
iosApp/iosApp/Assets.xcassets/NuvioTabLibrary.imageset/Contents.json
vendored
Normal file
16
iosApp/iosApp/Assets.xcassets/NuvioTabLibrary.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nuvio-tab-library.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
5
iosApp/iosApp/Assets.xcassets/NuvioTabLibrary.imageset/nuvio-tab-library.svg
vendored
Normal file
5
iosApp/iosApp/Assets.xcassets/NuvioTabLibrary.imageset/nuvio-tab-library.svg
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#000" d="M8.50989 2.00001H15.49C15.7225 1.99995 15.9007 1.99991 16.0565 2.01515C17.1643 2.12352 18.0711 2.78958 18.4556 3.68678H5.54428C5.92879 2.78958 6.83555 2.12352 7.94337 2.01515C8.09917 1.99991 8.27741 1.99995 8.50989 2.00001Z"/>
|
||||
<path fill="#000" d="M6.31052 4.72312C4.91989 4.72312 3.77963 5.56287 3.3991 6.67691C3.39117 6.70013 3.38356 6.72348 3.37629 6.74693C3.77444 6.62636 4.18881 6.54759 4.60827 6.49382C5.68865 6.35531 7.05399 6.35538 8.64002 6.35547H15.5321C17.1181 6.35538 18.4835 6.35531 19.5639 6.49382C19.9833 6.54759 20.3977 6.62636 20.7958 6.74693C20.7886 6.72348 20.781 6.70013 20.773 6.67691C20.3925 5.56287 19.2522 4.72312 17.8616 4.72312H6.31052Z"/>
|
||||
<path fill="#000" fill-rule="evenodd" d="M8.67239 7.54204H15.3276C18.7024 7.54204 20.3898 7.54204 21.3377 8.52887C22.2855 9.5157 22.0625 11.0403 21.6165 14.0896L21.1935 16.9811C20.8437 19.3724 20.6689 20.568 19.7717 21.284C18.8745 22 17.5512 22 14.9046 22H9.09536C6.44881 22 5.12553 22 4.22834 21.284C3.33115 20.568 3.15626 19.3724 2.80648 16.9811L2.38351 14.0896C1.93748 11.0403 1.71447 9.5157 2.66232 8.52887C3.61017 7.54204 5.29758 7.54204 8.67239 7.54204ZM8 18.0001C8 17.5859 8.3731 17.2501 8.83333 17.2501H15.1667C15.6269 17.2501 16 17.5859 16 18.0001C16 18.4144 15.6269 18.7502 15.1667 18.7502H8.83333C8.3731 18.7502 8 18.4144 8 18.0001Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
16
iosApp/iosApp/Assets.xcassets/NuvioTabProfile.imageset/Contents.json
vendored
Normal file
16
iosApp/iosApp/Assets.xcassets/NuvioTabProfile.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nuvio-tab-profile.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
3
iosApp/iosApp/Assets.xcassets/NuvioTabProfile.imageset/nuvio-tab-profile.svg
vendored
Normal file
3
iosApp/iosApp/Assets.xcassets/NuvioTabProfile.imageset/nuvio-tab-profile.svg
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#000" d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm0 2c-2.67 0-8 1.34-8 4v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 234 B |
16
iosApp/iosApp/Assets.xcassets/NuvioTabSearch.imageset/Contents.json
vendored
Normal file
16
iosApp/iosApp/Assets.xcassets/NuvioTabSearch.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nuvio-tab-search.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
3
iosApp/iosApp/Assets.xcassets/NuvioTabSearch.imageset/nuvio-tab-search.svg
vendored
Normal file
3
iosApp/iosApp/Assets.xcassets/NuvioTabSearch.imageset/nuvio-tab-search.svg
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#000" fill-rule="evenodd" d="M4 9a5 5 0 1 1 10 0A5 5 0 0 1 4 9Zm5-7a7 7 0 1 0 4.2 12.6.999.999 0 0 0 .093.107l3 3a1 1 0 0 0 1.414-1.414l-3-3a.999.999 0 0 0-.107-.093A7 7 0 0 0 9 2Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue