diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 70e8b7f1..c449a4d3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -23,8 +23,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp import androidx.datastore.core.DataStore +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModel -import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -46,6 +47,7 @@ import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver import com.github.damontecres.wholphin.services.RefreshRateService +import com.github.damontecres.wholphin.services.ServerEventListener import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupNavigationManager import com.github.damontecres.wholphin.services.UpdateChecker @@ -113,6 +115,14 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var tvProviderSchedulerService: TvProviderSchedulerService + // Note: unused but injected to ensure it is created + @Inject + lateinit var serverEventListener: ServerEventListener + + // Note: unused but injected to ensure it is created + @Inject + lateinit var datePlayedInvalidationService: DatePlayedInvalidationService + private var signInAuto = true @OptIn(ExperimentalTvMaterial3Api::class) @@ -232,13 +242,12 @@ class MainActivity : AppCompatActivity() { var showContent by remember { mutableStateOf(true) } - if (!preferences.appPreferences.signInAutomatically) { - LifecycleStartEffect(Unit) { - onStopOrDispose { - showContent = false - } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + if (!preferences.appPreferences.signInAutomatically) { + showContent = false } } + if (showContent) { ApplicationContent( user = current.user, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt index bf82bbf3..943789db 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt @@ -14,4 +14,5 @@ data class LocalTrailer( data class RemoteTrailer( override val name: String, val url: String, + val subtitle: String?, ) : Trailer diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaybackLifecycleObserver.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaybackLifecycleObserver.kt index 86906280..3c32a497 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaybackLifecycleObserver.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaybackLifecycleObserver.kt @@ -20,6 +20,10 @@ class PlaybackLifecycleObserver private var wasPlaying: Boolean? = null override fun onStart(owner: LifecycleOwner) { + val lastDest = navigationManager.backStack.lastOrNull() + if (lastDest is Destination.Playback || lastDest is Destination.PlaybackList) { + navigationManager.goBack() + } wasPlaying = null } @@ -40,9 +44,6 @@ class PlaybackLifecycleObserver } override fun onStop(owner: LifecycleOwner) { - if (navigationManager.backStack.lastOrNull() is Destination.Playback) { - navigationManager.goBack() - } themeSongPlayer.stop() } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt index 67b541fc..be6e461e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt @@ -95,13 +95,13 @@ class RefreshRateService } catch (ex: InterruptedException) { Timber.w(ex, "Exception waiting for refresh rate switch") } - val targetRate = (targetMode.refreshRate * 100).roundToInt() + val targetRate = (targetMode.refreshRate * 1000).roundToInt() val isSeamless = - targetRate == (display.mode.refreshRate * 100).roundToInt() || + targetRate == (currentDisplayMode.refreshRate * 1000).roundToInt() || if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - display.mode.alternativeRefreshRates - .map { (it * 100).roundToInt() } - .any { targetRate % it == 0 } + currentDisplayMode.alternativeRefreshRates + .map { (it * 1000).roundToInt() } + .any { it % targetRate == 0 } } else { false } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt index a5f54815..e6bcb867 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt @@ -104,7 +104,9 @@ class ThemeSongPlayer } fun stop() { - Timber.v("Stopping theme song") - player.stop() + if (player.isPlaying) { + Timber.v("Stopping theme song") + player.stop() + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt index 1af38ddd..856493f6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt @@ -22,31 +22,53 @@ class TrailerService @param:ApplicationContext private val context: Context, private val api: ApiClient, ) { - suspend fun getTrailers(item: BaseItem): List { - val remoteTrailers = - item.data.remoteTrailers - ?.mapNotNull { t -> - t.url?.let { url -> - val name = - t.name - // TODO would be nice to clean up the trailer name + fun getRemoteTrailers(item: BaseItem): List = + item.data.remoteTrailers + ?.mapNotNull { t -> + t.url?.let { url -> + val name = + t.name + // TODO would be nice to clean up the trailer name // ?.replace(item.name ?: "", "") // ?.removePrefix(" - ") - ?: context.getString(R.string.trailer) - RemoteTrailer(name, url) - } - }.orEmpty() - .sortedBy { it.name } - val localTrailerCount = item.data.localTrailerCount ?: 0 + ?: context.getString(R.string.trailer) + val subtitle = + when (url.toUri().host) { + "youtube.com", "www.youtube.com" -> "YouTube" + else -> null + } + RemoteTrailer(name, url, subtitle) + } + }.orEmpty() + .sortedWith( + compareBy( + { + // Try to show official trailers first & teasers last + when { + it.name.contains("Official Trailer", true) -> 0 + it.name.contains("Official Theatrical Trailer", true) -> 0 + it.name.contains("Teaser", true) -> 10 + it.name.contains("Trailer", true) -> 1 + else -> 5 + } + }, + { + it.name + }, + ), + ) + + suspend fun getLocalTrailers(item: BaseItem): List { + val localTrailerCount = item.data.localTrailerCount ?: return emptyList() val localTrailers = if (localTrailerCount > 0) { api.userLibraryApi.getLocalTrailers(item.id).content.map { - LocalTrailer(BaseItem.Companion.from(it, api)) + LocalTrailer(BaseItem.from(it, api)) } } else { listOf() } - return localTrailers + remoteTrailers + return localTrailers } companion object { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt index 4eba1b32..31754ef9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt @@ -28,9 +28,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.compose.LifecycleEventEffect import androidx.media3.common.Player import coil3.request.ErrorResult import com.github.damontecres.wholphin.data.model.BaseItem @@ -182,10 +180,10 @@ fun RequestOrRestoreFocus( debugKey?.let { Timber.v("RequestOrRestoreFocus: %s", it) } focusRequester.tryRequestFocus() } - LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { - debugKey?.let { Timber.v("RequestOrRestoreFocus onResume: %s", it) } - focusRequester.tryRequestFocus() - } +// LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { +// debugKey?.let { Timber.v("RequestOrRestoreFocus onResume: %s", it) } +// focusRequester.tryRequestFocus() +// } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index eef5c9ae..20270b94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -169,6 +169,7 @@ class CollectionFolderViewModel context.getString(R.string.error_loading_collection, itemId), ) + Dispatchers.IO, ) { + super.itemId = itemId itemId.toUUIDOrNull()?.let { fetchItem(it) } @@ -206,7 +207,7 @@ class CollectionFolderViewModel ) { if (collectionFilter.useSavedLibraryDisplayInfo) { serverRepository.currentUser.value?.let { user -> - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launchIO { val libraryDisplayInfo = LibraryDisplayInfo( userId = user.rowId, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index bfb098d5..4b985056 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -239,47 +240,48 @@ fun DialogPopupContent( ) { val elevatedContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation) - LazyColumn( + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier -// .widthIn(min = 520.dp, max = 300.dp) -// .dialogFocusable() .graphicsLayer { this.clip = true this.shape = RoundedCornerShape(28.0.dp) }.drawBehind { drawRect(color = elevatedContainerColor) } .padding(PaddingValues(24.dp)), ) { - stickyHeader { - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - } - items(dialogItems) { - when (it) { - is DialogItemDivider -> { - HorizontalDivider(Modifier.height(16.dp)) - } + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + LazyColumn( + modifier = Modifier, + ) { + items(dialogItems) { + when (it) { + is DialogItemDivider -> { + HorizontalDivider(Modifier.height(16.dp)) + } - is DialogItem -> { - ListItem( - selected = false, - enabled = !waiting && it.enabled, - onClick = { - if (dismissOnClick) { - onDismissRequest.invoke() - } - it.onClick.invoke() - }, - headlineContent = it.headlineContent, - overlineContent = it.overlineContent, - supportingContent = it.supportingContent, - leadingContent = it.leadingContent, - trailingContent = it.trailingContent, - modifier = Modifier, - ) + is DialogItem -> { + ListItem( + selected = false, + enabled = !waiting && it.enabled, + onClick = { + if (dismissOnClick) { + onDismissRequest.invoke() + } + it.onClick.invoke() + }, + headlineContent = it.headlineContent, + overlineContent = it.overlineContent, + supportingContent = it.supportingContent, + leadingContent = it.leadingContent, + trailingContent = it.trailingContent, + modifier = Modifier, + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt index ec615a1d..0d825f13 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt @@ -21,7 +21,10 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Refresh import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -40,6 +43,7 @@ import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.data.SortAndDirection @@ -59,10 +63,12 @@ fun ExpandablePlayButtons( resumePosition: Duration, watched: Boolean, favorite: Boolean, + trailers: List?, playOnClick: (position: Duration) -> Unit, watchOnClick: () -> Unit, favoriteOnClick: () -> Unit, moreOnClick: () -> Unit, + trailerOnClick: (Trailer) -> Unit, buttonOnFocusChanged: (FocusState) -> Unit, modifier: Modifier = Modifier, ) { @@ -134,6 +140,16 @@ fun ExpandablePlayButtons( ) } + if (trailers != null) { + item("trailers") { + TrailerButton( + trailers = trailers, + trailerOnClick = trailerOnClick, + modifier = Modifier.onFocusChanged(buttonOnFocusChanged), + ) + } + } + // More button item("more") { ExpandablePlayButton( @@ -229,6 +245,7 @@ fun ExpandableFaButton( ), contentPadding = DefaultButtonPadding, interactionSource = interactionSource, + enabled = enabled, ) { Box( modifier = @@ -255,6 +272,42 @@ fun ExpandableFaButton( } } +@Composable +fun TrailerButton( + trailers: List, + trailerOnClick: (Trailer) -> Unit, + modifier: Modifier = Modifier, +) { + var showDialog by remember { mutableStateOf(false) } + ExpandableFaButton( + title = + if (trailers.isEmpty()) { + R.string.no_trailers + } else if (trailers.size == 1) { + R.string.play_trailer + } else { + R.string.trailers + }, + iconStringRes = R.string.fa_film, + enabled = trailers.isNotEmpty(), + onClick = { + if (trailers.size == 1) { + trailerOnClick.invoke(trailers.first()) + } else { + showDialog = true + } + }, + modifier = modifier, + ) + if (showDialog) { + TrailerDialog( + onDismissRequest = { showDialog = false }, + trailers = trailers, + onClick = trailerOnClick, + ) + } +} + @PreviewTvSpec @Composable private fun ExpandablePlayButtonsPreview() { @@ -268,6 +321,8 @@ private fun ExpandablePlayButtonsPreview() { favoriteOnClick = {}, moreOnClick = {}, buttonOnFocusChanged = {}, + trailers = listOf(), + trailerOnClick = {}, modifier = Modifier, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TrailerDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TrailerDialog.kt new file mode 100644 index 00000000..0b17491b --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TrailerDialog.kt @@ -0,0 +1,51 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import androidx.tv.material3.Text +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.LocalTrailer +import com.github.damontecres.wholphin.data.model.RemoteTrailer +import com.github.damontecres.wholphin.data.model.Trailer + +@Composable +fun TrailerDialog( + onDismissRequest: () -> Unit, + trailers: List, + onClick: (Trailer) -> Unit, +) { + val trailersStr = stringResource(R.string.play_trailer) + val localStr = stringResource(R.string.local) + val externalStr = stringResource(R.string.external_track) + val params = + remember(trailers) { + DialogParams( + fromLongClick = false, + title = trailersStr, + items = + trailers.map { trailer -> + DialogItem( + headlineContent = { + Text(trailer.name) + }, + supportingContent = { + val subtitle = + when (trailer) { + is LocalTrailer -> localStr + is RemoteTrailer -> trailer.subtitle ?: externalStr + } + Text( + text = subtitle, + ) + }, + onClick = { onClick.invoke(trailer) }, + ) + }, + ) + } + DialogPopup( + params = params, + onDismissRequest = onDismissRequest, + ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index 19f8f6fd..53b6b9b8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -196,210 +196,224 @@ fun CardGrid( } } - var longPressing by remember { mutableStateOf(false) } - Row( -// horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = - modifier - .fillMaxSize() - .onKeyEvent { - if (DEBUG) Timber.d("onKeyEvent: ${it.nativeKeyEvent}") - if (useBackToJump && it.key == Key.Back && it.nativeKeyEvent.isLongPress) { - longPressing = true - val newPosition = previouslyFocusedIndex - if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition") - if (newPosition > 0) { - focusOn(newPosition) - scope.launch(ExceptionHandler()) { - gridState.scrollToItem(newPosition, -columns) - firstFocus.tryRequestFocus() - } - } - return@onKeyEvent true - } else if (it.type == KeyEventType.KeyUp) { - if (longPressing && it.key == Key.Back) { - longPressing = false - return@onKeyEvent true - } - longPressing = false - } - if (it.type != KeyEventType.KeyUp) { - return@onKeyEvent false - } else if (useBackToJump && it.key == Key.Back && focusedIndex > 0) { - jumpToTop() - return@onKeyEvent true - } else if (isPlayKeyUp(it)) { - val item = pager.getOrNull(focusedIndex) - if (item?.playable == true) { - Timber.v("Clicked play on ${item.id}") - onClickPlay.invoke(focusedIndex, item) - } - return@onKeyEvent true - } else if (useJumpRemoteButtons && isForwardButton(it)) { - jump(jump1) - return@onKeyEvent true - } else if (useJumpRemoteButtons && isBackwardButton(it)) { - jump(-jump1) - return@onKeyEvent true - } else { - return@onKeyEvent false - } - }, - ) { - if (showJumpButtons && pager.isNotEmpty()) { - JumpButtons( - jump1 = jump1, - jump2 = jump2, - jumpClick = { jump(it) }, - modifier = Modifier.align(Alignment.CenterVertically), + if (pager.isEmpty()) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxSize(), + ) { + Text( + text = stringResource(R.string.no_results), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, ) } - Box( - modifier = Modifier.weight(1f), - ) { - LazyVerticalGrid( - columns = GridCells.Fixed(columns), - horizontalArrangement = Arrangement.spacedBy(spacing), - verticalArrangement = Arrangement.spacedBy(spacing), - state = gridState, - contentPadding = PaddingValues(16.dp), - modifier = - Modifier - .fillMaxSize() - .focusGroup() - .focusRestorer(firstFocus) - .focusProperties { - onExit = { - // Leaving the grid, so "forget" the position -// focusedIndex = -1 - } - onEnter = { - if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) { - focusedIndex = startPosition + } else { + var longPressing by remember { mutableStateOf(false) } + Row( +// horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + modifier + .fillMaxSize() + .onKeyEvent { + if (DEBUG) Timber.d("onKeyEvent: ${it.nativeKeyEvent}") + if (useBackToJump && it.key == Key.Back && it.nativeKeyEvent.isLongPress) { + longPressing = true + val newPosition = previouslyFocusedIndex + if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition") + if (newPosition > 0) { + focusOn(newPosition) + scope.launch(ExceptionHandler()) { + gridState.scrollToItem(newPosition, -columns) + firstFocus.tryRequestFocus() } } - }, - ) { - items(pager.size) { index -> - val mod = - if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) { - if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index") - Modifier - .focusRequester(firstFocus) - .focusRequester(gridFocusRequester) - .focusRequester(alphabetFocusRequester) - } else { - Modifier + return@onKeyEvent true + } else if (it.type == KeyEventType.KeyUp) { + if (longPressing && it.key == Key.Back) { + longPressing = false + return@onKeyEvent true + } + longPressing = false } - val item = pager[index] - cardContent( - item, - { - if (item != null) { - focusedIndex = index - onClickItem.invoke(index, item) + if (it.type != KeyEventType.KeyUp) { + return@onKeyEvent false + } else if (useBackToJump && it.key == Key.Back && focusedIndex > 0) { + jumpToTop() + return@onKeyEvent true + } else if (isPlayKeyUp(it)) { + val item = pager.getOrNull(focusedIndex) + if (item?.playable == true) { + Timber.v("Clicked play on ${item.id}") + onClickPlay.invoke(focusedIndex, item) } - }, - { if (item != null) onLongClickItem.invoke(index, item) }, - mod - .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) - .onFocusChanged { focusState -> - if (DEBUG) { - Timber.v( - "$index isFocused=${focusState.isFocused}", - ) + return@onKeyEvent true + } else if (useJumpRemoteButtons && isForwardButton(it)) { + jump(jump1) + return@onKeyEvent true + } else if (useJumpRemoteButtons && isBackwardButton(it)) { + jump(-jump1) + return@onKeyEvent true + } else { + return@onKeyEvent false + } + }, + ) { + if (showJumpButtons && pager.isNotEmpty()) { + JumpButtons( + jump1 = jump1, + jump2 = jump2, + jumpClick = { jump(it) }, + modifier = Modifier.align(Alignment.CenterVertically), + ) + } + Box( + modifier = Modifier.weight(1f), + ) { + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + horizontalArrangement = Arrangement.spacedBy(spacing), + verticalArrangement = Arrangement.spacedBy(spacing), + state = gridState, + contentPadding = PaddingValues(16.dp), + modifier = + Modifier + .fillMaxSize() + .focusGroup() + .focusRestorer(firstFocus) + .focusProperties { + onExit = { + // Leaving the grid, so "forget" the position +// focusedIndex = -1 } - if (focusState.isFocused) { - // Focused, so set that up - focusOn(index) - positionCallback?.invoke(columns, index) - } else if (focusedIndex == index) { + onEnter = { + if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) { + focusedIndex = startPosition + } + } + }, + ) { + items(pager.size) { index -> + val mod = + if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) { + if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index") + Modifier + .focusRequester(firstFocus) + .focusRequester(gridFocusRequester) + .focusRequester(alphabetFocusRequester) + } else { + Modifier + } + val item = pager[index] + cardContent( + item, + { + if (item != null) { + focusedIndex = index + onClickItem.invoke(index, item) + } + }, + { if (item != null) onLongClickItem.invoke(index, item) }, + mod + .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) + .onFocusChanged { focusState -> + if (DEBUG) { + Timber.v( + "$index isFocused=${focusState.isFocused}", + ) + } + if (focusState.isFocused) { + // Focused, so set that up + focusOn(index) + positionCallback?.invoke(columns, index) + } else if (focusedIndex == index) { // savedFocusedIndex = index // // Was focused on this, so mark unfocused // focusedIndex = -1 - } - }, - ) + } + }, + ) + } } - } - if (pager.isEmpty()) { + if (pager.isEmpty()) { // focusedIndex = -1 - Box(modifier = Modifier.fillMaxSize()) { - Text( - text = stringResource(R.string.no_results), - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.align(Alignment.Center), - ) + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = stringResource(R.string.no_results), + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.align(Alignment.Center), + ) + } } - } - if (showFooter) { - // Footer - Box( - modifier = - Modifier - .align(Alignment.BottomCenter) - .background(AppColors.TransparentBlack50), - ) { - val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?" + if (showFooter) { + // Footer + Box( + modifier = + Modifier + .align(Alignment.BottomCenter) + .background(AppColors.TransparentBlack50), + ) { + val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?" // if (focusedIndex >= 0) { // focusedIndex + 1 // } else { // max(savedFocusedIndex, focusedIndexOnExit) + 1 // } - Text( - modifier = Modifier.padding(4.dp), - color = MaterialTheme.colorScheme.onBackground, - text = "$index / ${pager.size}", - ) + Text( + modifier = Modifier.padding(4.dp), + color = MaterialTheme.colorScheme.onBackground, + text = "$index / ${pager.size}", + ) + } } } - } - val context = LocalContext.current - val letters = context.getString(R.string.jump_letters) - // Letters - val currentLetter = - remember(focusedIndex) { - pager - .getOrNull(focusedIndex) - ?.sortName - ?.first() - ?.uppercaseChar() - ?.let { - if (it >= '0' && it <= '9') { - '#' - } else if (it >= 'A' && it <= 'Z') { - it - } else { - null - } - } - ?: letters[0] - } - if (showLetterButtons && pager.isNotEmpty()) { - AlphabetButtons( - letters = letters, - currentLetter = currentLetter, - modifier = - Modifier - .align(Alignment.CenterVertically) - .padding(end = 16.dp), - // Add end padding to push away from edge - letterClicked = { letter -> - scope.launch(ExceptionHandler()) { - val jumpPosition = - withContext(Dispatchers.IO) { - letterPosition.invoke(letter) + val context = LocalContext.current + val letters = context.getString(R.string.jump_letters) + // Letters + val currentLetter = + remember(focusedIndex) { + pager + .getOrNull(focusedIndex) + ?.sortName + ?.first() + ?.uppercaseChar() + ?.let { + if (it >= '0' && it <= '9') { + '#' + } else if (it >= 'A' && it <= 'Z') { + it + } else { + null } - Timber.d("Alphabet jump to $jumpPosition") - if (jumpPosition >= 0) { - pager.getOrNull(jumpPosition) - gridState.scrollToItem(jumpPosition) - focusOn(jumpPosition) - alphabetFocus = true } - } - }, - ) + ?: letters[0] + } + if (showLetterButtons && pager.isNotEmpty()) { + AlphabetButtons( + letters = letters, + currentLetter = currentLetter, + modifier = + Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp), + // Add end padding to push away from edge + letterClicked = { letter -> + scope.launch(ExceptionHandler()) { + val jumpPosition = + withContext(Dispatchers.IO) { + letterPosition.invoke(letter) + } + Timber.d("Alphabet jump to $jumpPosition") + if (jumpPosition >= 0) { + pager.getOrNull(jumpPosition) + gridState.scrollToItem(jumpPosition) + focusOn(jumpPosition) + alphabetFocus = true + } + } + }, + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt index d917b47a..65c9a0e9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt @@ -27,7 +27,7 @@ abstract class ItemViewModel( ) : ViewModel() { val item = MutableLiveData(null) lateinit var itemId: String - lateinit var itemUuid: UUID + var itemUuid: UUID? = null suspend fun fetchItem(itemId: UUID): BaseItem = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index 8af7ccab..b715af8d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -155,8 +155,10 @@ class PersonViewModel fun setFavorite(favorite: Boolean) { viewModelScope.launchIO { - favoriteWatchManager.setFavorite(itemUuid, favorite) - fetchAndSetItem(itemUuid) + itemUuid?.let { + favoriteWatchManager.setFavorite(it, favorite) + fetchAndSetItem(it) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index 493093da..c47570f6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.lifecycle.compose.LifecycleStartEffect import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem @@ -112,12 +111,12 @@ fun EpisodeDetails( LoadingState.Success -> { item?.let { ep -> - LifecycleStartEffect(destination.itemId) { + LifecycleResumeEffect(destination.itemId) { viewModel.maybePlayThemeSong( destination.itemId, preferences.appPreferences.interfacePreferences.playThemeSongs, ) - onStopOrDispose { + onPauseOrDispose { viewModel.release() } } @@ -335,6 +334,8 @@ fun EpisodeDetailsContent( } } }, + trailers = null, + trailerOnClick = {}, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 78876e38..6fc1cc19 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -8,9 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.runtime.Composable @@ -23,25 +20,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.lifecycle.compose.LifecycleStartEffect -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ExtrasItem import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter -import com.github.damontecres.wholphin.data.model.LocalTrailer import com.github.damontecres.wholphin.data.model.Person -import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.preferences.UserPreferences @@ -143,12 +133,12 @@ fun MovieDetails( LoadingState.Success -> { item?.let { movie -> - LifecycleStartEffect(destination.itemId) { + LifecycleResumeEffect(destination.itemId) { viewModel.maybePlayThemeSong( destination.itemId, preferences.appPreferences.interfacePreferences.playThemeSongs, ) - onStopOrDispose { + onPauseOrDispose { viewModel.release() } } @@ -437,6 +427,11 @@ fun MovieDetailsContent( } } }, + trailers = trailers, + trailerOnClick = { + position = TRAILER_ROW + trailerOnClick.invoke(it) + }, modifier = Modifier .fillMaxWidth() @@ -464,21 +459,6 @@ fun MovieDetailsContent( ) } } - if (trailers.isNotEmpty()) { - item { - TrailerRow( - trailers = trailers, - onClickTrailer = { - position = TRAILER_ROW - trailerOnClick.invoke(it) - }, - modifier = - Modifier - .fillMaxWidth() - .focusRequester(focusRequesters[TRAILER_ROW]), - ) - } - } if (chapters.isNotEmpty()) { item { ChapterRow( @@ -546,78 +526,3 @@ fun MovieDetailsContent( } } } - -@Composable -fun TrailerRow( - trailers: List, - onClickTrailer: (Trailer) -> Unit, - modifier: Modifier = Modifier, -) { - val state = rememberLazyListState() - val firstFocus = remember { FocusRequester() } - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier, - ) { - Text( - text = stringResource(R.string.trailers), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - ) - LazyRow( - state = state, - horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp), - modifier = - Modifier - .fillMaxWidth() - .focusRestorer(firstFocus), - ) { - itemsIndexed(trailers) { index, item -> - val cardModifier = - if (index == 0) { - Modifier.focusRequester(firstFocus) - } else { - Modifier - } - when (item) { - is LocalTrailer -> { - SeasonCard( - item = item.baseItem, - onClick = { onClickTrailer.invoke(item) }, - onLongClick = {}, - imageHeight = Cards.height2x3, - imageWidth = Dp.Unspecified, - showImageOverlay = false, - modifier = cardModifier, - ) - } - - is RemoteTrailer -> { - val subtitle = - when (item.url.toUri().host) { - "youtube.com", "www.youtube.com" -> "YouTube" - else -> null - } - SeasonCard( - title = item.name, - subtitle = subtitle, - name = item.name, - imageUrl = null, - isFavorite = false, - isPlayed = false, - unplayedItemCount = 0, - playedPercentage = 0.0, - onClick = { onClickTrailer.invoke(item) }, - onLongClick = {}, - modifier = cardModifier, - showImageOverlay = false, - imageHeight = Cards.height2x3, - imageWidth = Dp.Unspecified, - ) - } - } - } - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index be62b4aa..793ef8bb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -25,6 +25,7 @@ import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler @@ -116,16 +117,19 @@ class MovieViewModel item, userPreferencesService.getCurrent(), ) + val remoteTrailers = trailerService.getRemoteTrailers(item) withContext(Dispatchers.Main) { this@MovieViewModel.item.value = item chosenStreams.value = result + this@MovieViewModel.trailers.value = remoteTrailers loading.value = LoadingState.Success backdropService.submit(item) } viewModelScope.launchIO { - val trailers = trailerService.getTrailers(item) - withContext(Dispatchers.Main) { - this@MovieViewModel.trailers.value = trailers + trailerService.getLocalTrailers(item).letNotEmpty { localTrailers -> + withContext(Dispatchers.Main) { + this@MovieViewModel.trailers.value = localTrailers + remoteTrailers + } } } viewModelScope.launchIO { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt index ddaedd88..9b6d81df 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt @@ -45,6 +45,8 @@ fun FocusedEpisodeFooter( watchOnClick = watchOnClick, favoriteOnClick = favoriteOnClick, buttonOnFocusChanged = buttonOnFocusChanged, + trailers = null, + trailerOnClick = {}, modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 227f13bd..7546ca56 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R @@ -62,6 +62,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails +import com.github.damontecres.wholphin.ui.components.TrailerButton import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo @@ -70,7 +71,6 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson -import com.github.damontecres.wholphin.ui.detail.movie.TrailerRow import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberInt @@ -124,12 +124,12 @@ fun SeriesDetails( LoadingState.Success -> { item?.let { item -> - LifecycleStartEffect(destination.itemId) { + LifecycleResumeEffect(destination.itemId) { viewModel.maybePlayThemeSong( destination.itemId, preferences.appPreferences.interfacePreferences.playThemeSongs, ) - onStopOrDispose { + onPauseOrDispose { viewModel.release() } } @@ -411,6 +411,18 @@ fun SeriesDetailsContent( } }, ) + TrailerButton( + trailers = trailers, + trailerOnClick = trailerOnClick, + modifier = + Modifier.onFocusChanged { + if (it.isFocused) { + scope.launch(ExceptionHandler()) { + bringIntoViewRequester.bringIntoView() + } + } + }, + ) } } item { @@ -472,21 +484,6 @@ fun SeriesDetailsContent( ) } } - if (trailers.isNotEmpty()) { - item { - TrailerRow( - trailers = trailers, - onClickTrailer = { - position = TRAILER_ROW - trailerOnClick.invoke(it) - }, - modifier = - Modifier - .fillMaxWidth() - .focusRequester(focusRequesters[TRAILER_ROW]), - ) - } - } if (extras.isNotEmpty()) { item { ExtrasRow( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 9377aa1d..7575e3a4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -16,11 +16,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.map import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -37,7 +38,6 @@ import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.seasonEpisode -import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.flow.update import kotlinx.serialization.Serializable @@ -158,19 +158,21 @@ fun SeriesOverview( LoadingState.Success -> { series?.let { series -> - LaunchedEffect(Unit) { + RequestOrRestoreFocus( when (rowFocused) { - EPISODE_ROW -> episodeRowFocusRequester.tryRequestFocus() - CAST_AND_CREW_ROW -> castCrewRowFocusRequester.tryRequestFocus() - GUEST_STAR_ROW -> guestStarRowFocusRequester.tryRequestFocus() - } - } - LifecycleStartEffect(destination.itemId) { + EPISODE_ROW -> episodeRowFocusRequester + CAST_AND_CREW_ROW -> castCrewRowFocusRequester + GUEST_STAR_ROW -> guestStarRowFocusRequester + else -> episodeRowFocusRequester + }, + "series_overview", + ) + LifecycleResumeEffect(destination.itemId) { viewModel.maybePlayThemeSong( destination.itemId, preferences.appPreferences.interfacePreferences.playThemeSongs, ) - onStopOrDispose { + onPauseOrDispose { viewModel.release() } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 75d8344b..7f346d6c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -49,7 +49,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.AspectRatios -import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.PersonRow import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -107,6 +106,7 @@ fun SeriesOverviewContent( val scrollState = rememberScrollState() val scrollConnection = rememberDelayedNestedScroll() + var requestFocusAfterSeason by remember { mutableStateOf(false) } Box( modifier = modifier @@ -147,6 +147,7 @@ fun SeriesOverviewContent( onClick = { selectedTabIndex = it onChangeSeason.invoke(it) + requestFocusAfterSeason = true }, modifier = Modifier @@ -181,8 +182,14 @@ fun SeriesOverviewContent( } is EpisodeList.Success -> { + if (requestFocusAfterSeason) { + // Changing seasons, so move focus once the new episodes are loaded + LaunchedEffect(Unit) { + firstItemFocusRequester.tryRequestFocus() + requestFocusAfterSeason = false + } + } val state = rememberLazyListState(position.episodeRowIndex) - RequestOrRestoreFocus(firstItemFocusRequester) LazyRow( state = state, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index a1a4edf6..181e5b98 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -165,8 +165,9 @@ class SeriesViewModel } } } - + val remoteTrailers = trailerService.getRemoteTrailers(item) withContext(Dispatchers.Main) { + this@SeriesViewModel.trailers.value = remoteTrailers this@SeriesViewModel.position.update { it.copy( episodeRowIndex = @@ -179,9 +180,10 @@ class SeriesViewModel } if (seriesPageType == SeriesPageType.DETAILS) { viewModelScope.launchIO { - val trailers = trailerService.getTrailers(item) - withContext(Dispatchers.Main) { - this@SeriesViewModel.trailers.value = trailers + trailerService.getLocalTrailers(item).letNotEmpty { localTrailers -> + withContext(Dispatchers.Main) { + this@SeriesViewModel.trailers.value = localTrailers + remoteTrailers + } } } viewModelScope.launchIO { @@ -221,9 +223,6 @@ class SeriesViewModel ) { viewModelScope.launchIO { themeSongPlayer.playThemeFor(seriesId, playThemeSongs) - addCloseable { - themeSongPlayer.stop() - } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 067f7f6b..4cc98f8a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -234,7 +234,7 @@ fun HomePageContent( val listState = rememberLazyListState() val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } } - var focused by remember { mutableStateOf(false) } + var focused by rememberSaveable { mutableStateOf(false) } LaunchedEffect(homeRows) { if (!focused) { homeRows @@ -246,6 +246,8 @@ fun HomePageContent( listState.animateScrollToItem(position.row) focused = true } + } else { + rowFocusRequesters.getOrNull(position.row)?.tryRequestFocus() } } LaunchedEffect(position) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 39cee993..064434c4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -157,6 +157,7 @@ class PlaybackViewModel internal lateinit var item: BaseItem internal var forceTranscoding: Boolean = false private var activityListener: TrackActivityPlaybackListener? = null + private val jobs = mutableListOf() val nextUp = MutableLiveData() private var isPlaylist = false @@ -166,20 +167,22 @@ class PlaybackViewModel val subtitleSearchLanguage = MutableLiveData(Locale.current.language) init { - viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() } - player.addListener(this) - (player as? ExoPlayer)?.addAnalyticsListener(this) - addCloseable { player.removeListener(this@PlaybackViewModel) } - addCloseable { (player as? ExoPlayer)?.removeAnalyticsListener(this@PlaybackViewModel) } addCloseable { + player.removeListener(this@PlaybackViewModel) + (player as? ExoPlayer)?.removeAnalyticsListener(this@PlaybackViewModel) + this@PlaybackViewModel.activityListener?.let { it.release() player.removeListener(it) } + jobs.forEach { it.cancel() } + player.release() } - addCloseable { player.release() } - subscribe() - listenForTranscodeReason() + viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() } + player.addListener(this) + (player as? ExoPlayer)?.addAnalyticsListener(this) + jobs.add(subscribe()) + jobs.add(listenForTranscodeReason()) } /** @@ -874,7 +877,7 @@ class PlaybackViewModel } } - private fun listenForTranscodeReason() { + private fun listenForTranscodeReason(): Job = viewModelScope.launchIO { currentPlayback.collectLatest { if (it != null) { @@ -905,7 +908,6 @@ class PlaybackViewModel } } } - } private var lastInteractionDate: Date = Date() @@ -1026,11 +1028,13 @@ class PlaybackViewModel } fun release() { + Timber.v("release") activityListener?.release() player.release() + activityListener = null } - fun subscribe() { + fun subscribe(): Job = api.webSocket .subscribe() .onEach { message -> @@ -1085,7 +1089,6 @@ class PlaybackViewModel } } }.launchIn(viewModelScope) - } /** * Atomically update [currentMediaInfo] diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt index 4777e977..81eb8669 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt @@ -109,6 +109,7 @@ class MpvPlayer( Timber.v("config-dir=${context.filesDir.path}") MPVLib.addLogObserver(mpvLogger) + Timber.v("Creating MPVLib") MPVLib.create(context) MPVLib.setOptionString("config", "yes") MPVLib.setOptionString("config-dir", context.filesDir.path) @@ -127,6 +128,7 @@ class MpvPlayer( MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}") MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}") + Timber.v("Initializing MPVLib") MPVLib.initialize() MPVLib.setOptionString("force-window", "no") @@ -466,7 +468,6 @@ class MpvPlayer( override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException() override fun setVideoSurfaceView(surfaceView: SurfaceView?) { - throwIfReleased() if (DEBUG) Timber.v("setVideoSurfaceView") val surface = surfaceView?.holder?.surface if (surface != null && surface.isValid) { @@ -635,9 +636,9 @@ class MpvPlayer( val title = it.label ?: "External Subtitles" Timber.v("Adding external subtitle track '$title'") if (it.language.isNotNullOrBlank()) { - MPVLib.command(arrayOf("sub-add", url, "auto", title, it.language!!)) + MPVLib.command(arrayOf("sub-add", url, "select", title, it.language!!)) } else { - MPVLib.command(arrayOf("sub-add", url, "auto", title)) + MPVLib.command(arrayOf("sub-add", url, "select", title)) } } } @@ -837,7 +838,18 @@ class MpvPlayer( override fun handleMessage(msg: Message): Boolean { val cmd = MpvCommand.entries[msg.what] - Timber.v("handleMessage: cmd=$cmd") + Timber.d("handleMessage: cmd=$cmd") + if (isReleased && cmd != MpvCommand.DESTROY) { + Timber.w("Player is released, ignoring command %s", cmd) + return true + } + if (surface == null && !cmd.isLifecycle) { + // If libmpv isn't ready, re-enqueue the messages + // Note: this means nothing will play until it is attached to a surface, + // so MpvPlayer can't be used for background audio/music playback + Timber.v("MPV is not initialized/attached yet, requeue cmd %s", cmd) + internalHandler.sendMessageDelayed(Message.obtain(msg), 50) + } when (cmd) { MpvCommand.PLAY_PAUSE -> { val playWhenReady = msg.obj as Boolean @@ -894,6 +906,7 @@ class MpvPlayer( MPVLib.detachSurface() MPVLib.setPropertyString("vo", "null") MPVLib.setPropertyString("force-window", "no") + Timber.d("Detached surface") } if (surface != null) { MPVLib.attachSurface(surface) @@ -1035,14 +1048,16 @@ private data class MediaAndPosition( val startPositionMs: Long, ) -enum class MpvCommand { - PLAY_PAUSE, - SEEK, - SET_TRACK_SELECTION, - SET_SPEED, - SET_SUBTITLE_DELAY, - LOAD_FILE, - ATTACH_SURFACE, - INITIALIZE, - DESTROY, +enum class MpvCommand( + val isLifecycle: Boolean, +) { + PLAY_PAUSE(false), + SEEK(false), + SET_TRACK_SELECTION(false), + SET_SPEED(false), + SET_SUBTITLE_DELAY(false), + LOAD_FILE(false), + ATTACH_SURFACE(true), + INITIALIZE(true), + DESTROY(true), } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt index a143d860..b7d28db1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt @@ -210,7 +210,7 @@ fun createDeviceProfile( "main", "baseline", "constrained baseline", - if (supportsAVCHigh10) "main 10" else null, + if (supportsAVCHigh10) "high 10" else null, ) } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 13f608d7..23dca91e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -239,7 +239,7 @@ Nützlich zum Debuggen Sende Absturzmeldung Zurückspringen wenn wiedergabe fortgesetzt wird - Überspringe Werbung verhalten + Werbung überspringen Vorspringen Überspringe Intro verhalten Überspringe Outro verhalten @@ -286,4 +286,39 @@ Entfernen Dolby Vision Dolby Atmos + Endet um %1$s + Server Adresse eingeben + Keine Server gefunden + Medieninformation + Abgespielt + MPV: Benutze gpu-next + mpv.conf bearbeiten + Änderungen verwerfen? + Abstand + PIN Eingeben + Automatisch anmelden + Profil benötigt PIN + PIN bestätigen + Falsch + PIN muss 4 Zeichen oder länger sein + PIN wird entfernt + Anzeigeoptionen + Spalten + Abstände + Seitenverhältnis + Details anzeigen + Bild Typ + Automatisch + Verwende Benutzername/Passwort + Anmelden + Titel + Profil + Auflösung + Bildrate + Sprache + Kanäle + Ja + Nein + Titel anzeigen + Wiederholen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f1ca329f..ce48243d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,8 +1,8 @@ - Favorito - Agregar servidor - Agregar usuario + Añadir a Favoritos + Añadir servidor + Añadir usuario Audio Lugar de nacimiento Tasa de bits @@ -170,8 +170,8 @@ Canciones principales - - + + Vídeos temáticos @@ -303,4 +303,76 @@ Fuente Fondo Clasificación parental + Termina en %1$s + Introduzca la dirección del servidor + No se encontraron servidores + Información multimedia + MPV: Usar gpu-next + Editar mpv.conf + ¿Descartar los cambios? + Margen + Registro detallado + Introducir PIN + Iniciar sesión automáticamente + Iniciar sesión a través del servidor + Pulsa el centro para confirmar + Requerir PIN para el perfil + Confirmar PIN + Incorrecto + El PIN debe tener 4 teclas o más + Se eliminará el PIN + Tamaño de la caché de disco de imágenes (MB) + Opciones de visualización + Columnas + Espaciado + Relación de aspecto + Mostrar detalles + Tipo de imagen + + Estrellas invitadas + Tamaño del borde + Cambio de frecuencia de actualización + Automático + Usar nombre de usuario/contraseña + Iniciar sesión + General + Contenedor + Título + Codec + Perfil + Nivel + Resolución + Anamórfico + Entrelazado + Frecuencia de fotogramas + Profundidad de bits + Rango de vídeo + Tipo de rango de vídeo + Espacio de color + Transferencia de color + Primarias de color + Formato de píxel + Fotogramas de referencia + NAL + Idioma + Distribución + Canales + Frecuencia de muestreo + AVC + + No + SDR + HDR + HDR10 + HDR10+ + HLG + bit + Hz + Mostrar títulos + Repetir + Mostrar primero los canales favoritos + Ordenar los canales por vistos recientemente + Programas codificados por colores + Retardo de subtítulos + Estilo del fondo diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 59aa1f7b..4809541d 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -358,4 +358,5 @@ Sisesta serveri aadress Ühtegi serverit ei leidu Tausta dekoratsioon + Lõppeb kell %1$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ba3fb676..539ca50c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -370,4 +370,9 @@ Images de référence Trier les chaînes par date de visionnage Classer les programmes par code couleur + Terminera à %1$s + Entrez l\'adresse du serveur + Aucun serveur trouvé + Décalage des sous-titres + Style d\'arrière-plan diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 7d0320bb..e9c73c87 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1,36 +1,36 @@ Névjegy - Kedvenc + Hozzáadás kedvencekhez Szerver hozzáadása Felhasználó hozzáadása - Audio + Hang Születési hely Bitráta Született Mégse Fejezetek - Válassz %1$s - Kép gyorsítótár törlése + %1$s kiválasztása + Képgyorsítótár törlése Gyűjtemény Gyűjtemények Közösségi értékelés - Hiba történt! Nyomd meg a gombot, hogy elküldd a naplófájlokat a szervernek. + Hiba történt! Nyomja meg a gombot, hogy elküldje a naplófájlokat a szervernek. Megerősít Kritikusi értékelés %.1f másodperc Alapértelmezett Törlés - Rendezte %1$s + Rendezte: %1$s Rendező Letiltva Felfedezett szerverek Letöltés folyamatban… Engedélyezve - Add meg a szerver IP-t vagy URL-t + Adja meg a szerver IP-t vagy URL-t Epizódok - Hiba a gyűjtemény betöltése során: %1$s + Hiba a következő gyűjtemény betöltése során: %1$s Külső Kedvencek Aktív felvételek @@ -38,28 +38,28 @@ Sorozatfelvétel leállítása Reklám Megtekintés folytatása - Halott + Elhunyt Méret - Kényszerített + Kiegészítő Műfajok Ugrás a sorozathoz - Ugrás + Ugrás ide: Lejátszási gombok elrejtése - Hibajavítási infó elrejtése + Hibakeresési infó elrejtése Elrejt Kezdőlap Azonnal - Intro + Intró #ABCDEFGHIJKLMNOPQRSTUVWXYZ Könyvtár Lincenc információk Élő TV Betöltés… - Megjelölöd az egész sorozatot megnézettként? - Megjelölöd az egész sorozatot megnézetlenként? + Megjelöli az egész sorozatot megnézettként? + Megjelöli az egész sorozatot megnézetlenként? Jelölés megnézetlenként Jelölés megnézettként - Max bitráta + Maximális bitráta Hasonlóak Továbbiak Filmek @@ -70,15 +70,15 @@ Nincs időzített felvétel Nincs elérhető frissítés Nincs - Outro + Stáblista Elérési út - Emberek + Stábtagok Lejátszások száma Lejátszás innen Lejátszás Lejátszás hibakeresési adatainak mutatása Lejátszási sebesség - Visszajátszás + Lejátszás Lejátszási lista Lejátszási listák Előnézet @@ -87,9 +87,9 @@ Nemrég hozzáadva: %1$s Nemrég hozzáadott Nemrég rögzített - Nemrég kiadott + Nemrég megjelent Ajánlott - Program felvétele + Műsor felvétele Sorozat felvétele Törlés a kedvencekből Újraindítás @@ -100,117 +100,263 @@ Keresés… Szerver kiválasztása Felhasználó kiválasztása - Hibakeresési info megjelenítése + Hibakeresési infó megjelenítése Következő mutatása Megmutat Véletlenszerű Átugrás - Hozzáadva + Hozzáadás dátuma Epizód hozzáadásának dátuma Lejátszás dátuma Megjelenés dátuma Név - Random + Véletlenszerűen Stúdiók Beküld - A letöltés sok időt vesz igénybe, előfordulhat, hogy újra kell indítanod a lejátszást + A letöltés sok időt vesz igénybe, előfordulhat, hogy újra kell indítani a lejátszást Felirat Feliratok Ajánlások - Váltás szerverek között + Váltás másik szerverre Váltás - Legjobbra értékelt nem megtekintett + Legjobbra értékelt, nem megnézett Előzetes Előzetesek - + DVR Időzítés Műsorújság Évad Évadok Sorozatok - Felület + Kezelőfelület Ismeretlen Frissítések Verzió - Képarány + Kép méretezése Videó Videók - Nézd élőben + Nézze élőben %1$d éves Lejátszás átalakítással Aktuális idő mutatása - Sorrend epízód vetítése alapján + Sorrend epizód vetítése alapján Új lejátszási lista létrehozása Hozzáadás lejátszási listához Szüneteltetés egy klikkel - Nyomd a D-Pad közepét megállításhoz/lejátszáshoz + Nyomja le a D-Pad közepét megállításhoz/lejátszáshoz Dőlt betűk - Betűtípus - Háttérszín + Betűk stílusa + Háttér Sikeres - Szülői besorolás - Műsoridő + Korhatár-besorolás + Hossz Extrák Egyéb - + Egyebek Színfalak mögött - + - Téma zenék - + Főcímdal + Főcímdalok - Téma videók - + Téma videó + Téma videók - Klippek - + Klip + Klipek - Törölt jelenetek - + Törölt jelenet + Törölt jelenetek - Interjúk - + Interjú + Interjúk - Jelenetek - + Jelenet + Jelenetek - Minták - + Minta + Minták - Rövidfilmek - + Rövidfilm + Rövidfilmek - Rövidek - + Rövid + Rövidek %s letöltés - %s letöltések + %s letöltés %d óra - %d órák + %d óra %d elem - %d elemek + %d elem %d másodperc - %d másodpercek + %d másodperc + %1$s-kor ér véget + Nem található szerver + Összefoglaló + Médiainformáció + A készülék támogatja az AC3/Dolby Digital formátumot + Haladó beállítások + Haladó kezelőfelület-beállítások + Alkalmazás témája + Frissítések automatikus keresése + Késleltetés a következő elem lejátszása előtt + Következő elem automatikus lejátszása + Frissítések keresése + Csak sorozatok esetén + ASS feliratok közvetlen lejátszása + PGS feliratok közvetlen lejátszása + Mindig konvertálja sztereóvá + FFmpeg dekóder modul használata + Tartalom alapértelmezett méretezése + Frissítés telepítése + Telepített verzió + Maximális elemek soronként a kezdőlapon + Válassza ki az alapértelmezett elemeket, amelyek megjelennek, a többi el lesz rejtve + Navigációs menü elemeinek testreszabása + Kattintson az oldalak közti váltáshoz + Automatikus leállítás inaktivitás esetén + Főcímdal lejátszása + Kiválasztott lapok megjegyzése + Újranézés engedélyezése a Következő menüben + Előre/hátra ugrás intervallum + Hibakereséshez hasznos + Naplófájlok küldése a jelenlegi szerverre + Megpróbálja elküldeni az legutóbb csatlakozott szerverre + Összeomlási napló küldése + Beállítások + Visszaugrás a lejátszás folytatásakor + Visszaugrás + Reklám-átugrás viselkedés + Előreugrás + Intró-átugrás viselkedés + Stáblista-átugrás viselkedés + Előzetes-átugrás viselkedés + Összefoglaló-átugrás viselkedés + Frissítés érhető el + Az alkalmazás frissítéseinek ellenőrzésére használt URL + Frissítés URL + Betűméret + Betűszín + Betűk áttetszősége + Szegély stílusa + Szegély színe + Háttér áttetszősége + Háttér típusa + Felirat stílusa + Háttér színe + Visszaállítás + Félkövér betűk + MPV: hardveres dekódolás használata + Tiltsa le, ha összeomlást tapasztal + MPV beállítások + ExoPlayer beállítások + Szegmens átugrása + Reklám átugrása + Előzetes átugrása + Összefoglaló átugrása + Stáblista átugrása + Intró átugrása + Lejátszott + Szűrő + Év + Évtized + Eltávolítás + Dolby Vision + Dolby Atmos + MPV: gpu-next használata + mpv.conf szerkesztése + Elveti a változtatásokat? + Függőleges eltolás + Részletes naplózás + Adja meg a PIN-kódot + Automatikus bejelentkezés + Bejelentkezés szerveren keresztül + Nyomja meg a középső gombot a megerősítéshez + Kérjen PIN-kódot a profillal történő bejelentkezéshez + PIN-kód megerősítése + Helytelen + A PIN-kódnak legalább 4 karakterből kell állnia + PIN-kód eltávolítása + Nézetbeállítások + Oszlopok + Helyköz + Képarány + Részletek mutatása + Kép típusa + Vendégszereplők + Szegély mérete + Tartalomhoz igazodó képfrissítési gyakoriság + Automatikus + Felhasználónév/jelszó használata + Bejelentkezés + Általános + Konténer + Cím + Kodek + Profil + Szint + Felbontás + Anamorfikus + Váltottsoros + Képfrissítés + Színmélység + Feketeszint tartomány + Feketeszint tartomány típusa + Színtér + Színátviteli jellemző + Alapszínek + Pixelformátum + Referencia-képkockák + NAL + Nyelv + Elrendezés + Csatornák + Mintavételezés + AVC + Igen + Nem + SDR + HDR + HDR10 + HDR10+ + HLG + bit + Hz + Címek mutatása + Ismétlés + A kedvenc csatornákat mutassa először + Legutóbb nézett csatornák előre sorolása + Műsorok kategória szerinti színezése + Felirat késleltetése + Háttér stílusa + Adja meg a szerver címét + + Lejátszás backend + Kép gyorsítótár méret (MB) + + Navigációs menü oldalainak váltása fókusznál + Lejátszás felülbírálatai diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index fc1f6866..456bee92 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -332,4 +332,8 @@ Resolusi Sampel rate Jarak + Berakhir pada %1$s + Masukkan alamat server + Server tidak ditemukan + Penundaan subtitel diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index faf958b2..c8837cdf 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -371,4 +371,8 @@ Ordina canali per ultimo guardato Codifica colori per i programmi Ritardo sottotitoli + Inserisci indirizzo server + Nessun server trovato + Stile backdrop + Termina alle %1$s diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 045e125f..51d5978a 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -1,3 +1,33 @@ + אודות + הקלטות פעילות + הוספה למועדפים + הוסף שרת + הוסף משתמש + שמע + מקום לידה + קצב נתונים + בטל הקלטה + בטל הקלטת סדרה + בטל + פרקים + נקה זיכרון מטמון + אוסף + אוספים + מסחרי + דירוג קהילתי + אירעה שגיאה! לחץ על הכפתור על מנת לשלוח יומני אירועים לשרת שלך. + לאשר + המשך צפייה + דירוג מבקרים + %.1f שניות + ברירת מחדל + מחק + מת + בבימויו של %1$s + במאי + מושבת + נולד + בחר %1$s diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml new file mode 100644 index 00000000..43c74c5a --- /dev/null +++ b/app/src/main/res/values-kab/strings.xml @@ -0,0 +1,46 @@ + + + Ɣef + Imesli + Adig n tlalit + Aktum + Semmet + Ixfawen + Tagrumma + Tigrummiwin + Serggeg + Amezwer + Kkes + Yemmut + Ffer-it + Tazwara + #ABCDEFGHIJKLMNOPQRSTUVWXYZ + Tamkarḍit + Ales asekker + Kemmel + Sekles + Nadi + Yettnadi… + Sken-d + Isem + Agacuṛan + Lqem + Tavidyut + Tividyutin + Tasefsit + Agilal + Yedda + Aseggas + Awurman + Tuqqna + Amatu + Amagbar + Azwel + Akudak + Amaɣnu + Aswir + HDR10 + HDR10+ + HLG + Fren %1$s + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index fd8c6cce..b4826c5b 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -3,12 +3,12 @@ Om Aktive opptak Favoritt - Legg til Server - Legg til Brukar + Legg til server + Legg til brukar Lyd Fødestad Bitrate - Født + Fødd Avbryt opptak Avbryt opptak av serie Avbryt @@ -21,4 +21,342 @@ Slett Dødd Regissert av %1$s + Eininga støttar AC3/Dolby Digital + Tøm biletmellomlager + Reklame + Brukarvurdering + Ein feil har oppstått! Trykk på knappen for å sende loggar til servaren din. + Hald fram med å sjå + Meldevurdering + %.1f sekund + Regissør + Deaktivert + Funne servarar + + Lastar ned… + Aktivert + Oppgi servar-IP eller URL + Oppgi servaradresse + Episodar + Feil ved lasting av samling %1$s + Ekstern + Favorittar + Storleik + Tvungen + Sjangrar + Gå til serie + Gå til + Gøym avspelingskontrollar + Gøym feilsøkingsinfo + Gøym + Heim + Omgåande + Intro + #ABCDEFGHIJKLMNOPQRSTUVWXYZÆØÅ + Bibliotek + Lisensinformasjon + Direkte-TV + Lastar… + Marker heile serien som sett? + Marker heile serien som usett? + Marker som usett + Marker som sett + Maks bitrate + Meir som dette + Meir + Filmar + Namn + Neste + Ingen data + Ingen resultat + Ingen servarar funne + Ingen planlagde opptak + Ingen oppdatering tilgjengeleg + Outro + Filveg + Folk + Antall avspelingar + Spel av herfrå + Spel av + Vis feilsøkingsinfo for avspeling + Avspelingsfart + Avspeling + Speleliste + Spelelister + Førehandsvising + Innstillingar for brukarprofil + + Samandrag + Sist lagt til i %1$s + Sist lagt til + Sist tatt opp + Sist utgitt + Tilrådd + Ta opp program + Ta opp serie + Fjern favoritt + Start på nytt + Hald fram + Lagre + + Søk + Søker… + Vel servar + Vel brukar + Vis feilsøkingsinfo + Vis \'Neste\' + Serie + Tilfeldig rekkjefølgje + Hopp over + Dato lagt til + Dato episode lagt til + Dato spela av + Utginningsdato + Namn + Tilfeldig + Studio + Send inn + Undertekst + Undertekstar + Forslag + Byt servar + Byt brukar + Best vurderte usette + Trailer + + Trailerar + + + DVR-plan + Programoversikt + Sesong + Sesongar + TV-seriar + Grensesnitt + Ukjend + Oppdateringar + Versjon + Videoskalering + Video + Videoar + Sjå direkte + %1$d år gammal + Spel av med transkoding + Mediainformasjon + Vis klokke + Rekkjefølgje etter sendedato + Lag ny speleliste + Legg til i speleliste + Pause med eitt klikk + Trykk i midten på styreputa for å pause/spele av + Kursiv skrift + Skrifttype + Bakgrunn + Fullført + Aldersgrense + Speltid + Ekstra + + Anna + + + + Bak kulissane + + + + Temasongar + + + + Temavideoar + + + + Klipp + + + + Sletta sener + + + + Intervju + + + + Scener + + + + Smakebitar + + + + Bakomfilmar + + + + Kortfilmar + + + Avanserte innstillingar + Avansert grensesnitt + App-tema + Sjekk automatisk etter oppdateringar + Forseinking før neste vert spela av + Spel neste automatisk + Sjekk etter oppdateringar + Gjeld berre TV-seriar + Slå saman \'Hald fram med å sjå\' og \'Neste\' + Direkteavspeling av ASS-undertekstar + Direkteavspeling av PGS-undertekstar + Alltid nedmiks til stereo + Bruk FFmpeg-dekodermodul + Standard innhaldsskalering + Installer oppdatering + Installert versjon + Maks element på rader på heimskjermen + Tilpass element i navigasjonsskuffa + Klikk for å byte side + Byt side i navigasjonsskuffa ved fokus + Svevn-beskyttelse + Spel temamusikk + Overstyring av avspeling + Hugs valde faner + Tillat gjensjåing i \'Neste\' + Steglengde for søkelinje + Nyttig for feilsøking + Send app-loggar til gjeldande servar + Vil prøve å sende til sist tilkopla servar + Send krasjrapportar + Innstillingar + Hopp tilbake ved framhald av avspeling + Hopp tilbake + Handling for overhopping av reklame + Hopp framover + Handling for overhopping av intro + Handling for overhopping av outro + Handling for overhopping av førehandsvising + Handling for overhopping av samandrag + Oppdatering tilgjengeleg + URL for sjekk av app-oppdateringar + URL for oppdatering + Skriftstorleik + Skriftfarge + Gjennomsiktigheit for skrift + Kantstil + Kantfarge + Gjennomsiktigheit for bakgrunn + Bakgrunnsstil + Stil for undertekst + Bakgrunnsfarge + Nullstill + Feit skrift + Avspelingsmotor + MPV: Bruk maskinvaredekoding + Deaktiver dersom appen krasjar + MPV-val + ExoPlayer-val + Hopp over segment + Hopp over reklame + Hopp over førehandsvising + Hopp over samandrag + Hopp over outro + Hopp over intro + Sett + Filter + År + Tiår + Fjern + Dolby Vision + Dolby Atmos + MPV: Bruk gpu-next + Rediger mpv.conf + Forkast endringar? + Marg + Detaljert logging + Tast inn PIN + Logg inn automatisk + Logg inn via servar + Trykk i midten for å bekrefte + Krev PIN for profil + Bekreft PIN + Feil + PIN må vere 4 tastar eller lenger + Vil fjerne PIN + Storleik på biletmellomlager (MB) + Visingsval + Kolonner + Mellomrom + Bileteforhold + Vis detaljar + Bilettype + + Gjesteroller + Kantstorleik + Byting av bildeoppdateringsfrekvens + Automatisk + Bruk brukarnamn/passord + Logg inn + Generelt + Containar + Tittel + Kodek + Profil + Nivå + Oppløysing + Anamorfisk + Linjefletta + Bildefrekvens + Bit-dybde + Videoområde + Type videoområde + Fargerom + Fargeoverføring + Fargeprimærar + Pikselformat + Referanserammer + NAL + Språk + Layout + Kanalar + Samplingsfrekvens + AVC + Ja + Nei + SDR + HDR + HDR10 + HDR10+ + HLG + bit + Hz + Vis titlar + Repeter + Vis favorittkanalar først + Sorter kanalar etter sist sett + Fargekod program + Undertekstforseinking + Bakgrunnsstil + Ingen + Nedlastinga tek lang tid, du må kanskje starte avspelinga på nytt + + %s nedlasting + %s nedlastingar + + + %d time + %d timar + + + %d element + %d elementer + + + %d sekund + %d sekund + + Vel standardelementa som skal visast, andre vert skjulde + Sluttar %1$s diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 89c4b105..346cd2c6 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -371,4 +371,8 @@ Ordenar canais por visualizações recentes Codificação de cores para os programas Atraso da legenda + Insira o endereço do servidor + Nenhum servidor encontrado + Estilo de fundo + Termina às %1$s diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2d317635..e8223887 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -339,4 +339,8 @@ 按最近观看的频道排序 节目颜色标记 字幕延迟 + 输入服务器地址 + 未找到服务器 + 背景幕样式 + 结束于 %1$s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ef0da683..efa7096c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -183,8 +183,8 @@ #ABCDEFGHIJKLMNOPQRSTUVWXYZ 授權資訊 電視直播 - 標記整部電視劇為已觀看? - 標記整部電視劇為未觀看? + 標記整部劇為已觀看? + 標記整部劇為未觀看? 標記為未觀看 標記為已觀看 最大位元率 @@ -299,4 +299,48 @@ 使用帳號/密碼 登入 字幕同步 + 顯示標題 + 輸入伺服器位址 + 未找到伺服器 + 背景圖樣式 + 節目顏色標記 + 優先顯示最愛頻道 + 按最近觀看的頻道排序 + 結束於 %1$s + 一般 + 封裝格式 + 標題 + 編碼 + 解析度 + 變形寬銀幕 + 等級 + 編碼設定檔 + 語言 + + + SDR + HDR + HDR10 + HDR10+ + HLG + bit + Hz + 重複 + 隔行掃描 + 參考影格數 + 影格率 + NAL + 位元深度 + AVC + 聲道配置 + 聲道數 + 動態範圍 + 動態範圍類型 + 色彩空間 + 色彩轉換 + 色彩原色 + 像素格式 + 取樣率 + 進度條跳轉值 + 播放覆蓋設定 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7fb034d..c94bb868 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -399,6 +399,9 @@ Subtitle delay Backdrop style Resolution switching + Local + Play trailer + No trailers Discover Request