diff --git a/app/src/androidTest/java/com/github/damontecres/wholphin/test/InstrumentedBasicUiTests.kt b/app/src/androidTest/java/com/github/damontecres/wholphin/test/InstrumentedBasicUiTests.kt index 72cf2896..09dc6745 100644 --- a/app/src/androidTest/java/com/github/damontecres/wholphin/test/InstrumentedBasicUiTests.kt +++ b/app/src/androidTest/java/com/github/damontecres/wholphin/test/InstrumentedBasicUiTests.kt @@ -9,7 +9,9 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.pressKey +import androidx.navigation3.runtime.NavBackStack import com.github.damontecres.wholphin.MainContent +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.ScreensaverService import com.github.damontecres.wholphin.services.ScreensaverState import com.github.damontecres.wholphin.services.SetupDestination @@ -43,16 +45,17 @@ class InstrumentedBasicUiTests { @OptIn(ExperimentalTestApi::class) @Test fun myTest() { + val navigationManager = NavigationManager() + navigationManager.backStack = NavBackStack(Destination.Home()) // Start the app composeTestRule.setContent { WholphinTheme { MainContent( backStack = mutableListOf(SetupDestination.ServerList), - navigationManager = mockk(relaxed = true), + navigationManager = navigationManager, appPreferences = mockk(relaxed = true), backdropService = mockk(relaxed = true), screensaverService = screensaverService, - requestedDestination = Destination.Home(), modifier = Modifier, ) } 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 9cb740ab..e8791bde 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -24,6 +24,7 @@ import androidx.datastore.core.DataStore import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope +import androidx.navigation3.runtime.NavBackStack import androidx.tv.material3.ExperimentalTvMaterial3Api import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreference @@ -67,6 +68,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import okhttp3.OkHttpClient import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.serializer.toUUIDOrNull @@ -127,6 +129,11 @@ class MainActivity : AppCompatActivity() { private var signInAuto = true + private val json = + Json { + classDiscriminator = "_type" + } + @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -134,6 +141,23 @@ class MainActivity : AppCompatActivity() { Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") lifecycle.addObserver(playbackLifecycleObserver) + val backStackStr = savedInstanceState?.getString(KEY_BACK_STACK) + if (backStackStr != null) { + Timber.d("Restoring back stack") + var backStack = json.decodeFromString>(backStackStr) + val lastDest = backStack.lastOrNull() + if (lastDest is Destination.Playback || + lastDest is Destination.PlaybackList || + lastDest is Destination.Slideshow + ) { + backStack = backStack.toMutableList().apply { removeAt(lastIndex) } + } + navigationManager.backStack = NavBackStack(*backStack.toTypedArray()) + } else { + val startDestination = intent?.let(::extractDestination) ?: Destination.Home() + navigationManager.backStack = NavBackStack(startDestination) + } + viewModel.serverRepository.currentUser.observe(this) { user -> if (user?.hasPin == true) { window?.setFlags( @@ -206,17 +230,12 @@ class MainActivity : AppCompatActivity() { appThemeColors = appPreferences.interfacePreferences.appThemeColors, ) { ProvideLocalClock { - val requestedDestination = - remember(intent) { - intent?.let(::extractDestination) ?: Destination.Home() - } MainContent( backStack = setupNavigationManager.backStack, navigationManager = navigationManager, appPreferences = appPreferences, backdropService = backdropService, screensaverService = screensaverService, - requestedDestination = requestedDestination, modifier = Modifier.fillMaxSize(), ) } @@ -287,6 +306,8 @@ class MainActivity : AppCompatActivity() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) Timber.d("onSaveInstanceState") + val str = json.encodeToString(navigationManager.backStack.toList()) + outState.putString(KEY_BACK_STACK, str) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { @@ -362,6 +383,8 @@ class MainActivity : AppCompatActivity() { const val INTENT_SEASON_NUMBER = "seaNum" const val INTENT_SEASON_ID = "seaId" + private const val KEY_BACK_STACK = "backStack" + lateinit var instance: MainActivity private set } diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt index 5bade867..0b32b9ed 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt @@ -33,10 +33,8 @@ import com.github.damontecres.wholphin.services.ScreensaverService import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.ui.components.AppScreensaver import com.github.damontecres.wholphin.ui.nav.ApplicationContent -import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.setup.SwitchServerContent import com.github.damontecres.wholphin.ui.setup.SwitchUserContent -import com.github.damontecres.wholphin.ui.util.ProvideLocalClock @Composable fun MainContent( @@ -45,7 +43,6 @@ fun MainContent( appPreferences: AppPreferences, backdropService: BackdropService, screensaverService: ScreensaverService, - requestedDestination: Destination, modifier: Modifier = Modifier, ) { Surface( @@ -113,7 +110,6 @@ fun MainContent( ApplicationContent( user = current.user, server = current.server, - startDestination = requestedDestination, navigationManager = navigationManager, preferences = preferences, modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt index a4c09ad4..1a350905 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt @@ -22,9 +22,7 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.ScreensaverService -import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.ui.components.AppScreensaverContent import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.theme.WholphinTheme @@ -33,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import org.jellyfin.sdk.model.serializer.toUUIDOrNull +import timber.log.Timber import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -61,6 +60,7 @@ class WholphinDreamService : override fun onCreate() { super.onCreate() + Timber.d("onCreate") savedStateRegistryController.performRestore(null) lifecycleRegistry.currentState = Lifecycle.State.CREATED @@ -74,6 +74,7 @@ class WholphinDreamService : override fun onAttachedToWindow() { super.onAttachedToWindow() + Timber.d("onAttachedToWindow") val itemFlow = screensaverService.createItemFlow(lifecycleScope) setContentView( ComposeView(this).apply { @@ -109,11 +110,13 @@ class WholphinDreamService : override fun onDreamingStarted() { super.onDreamingStarted() + Timber.d("onDreamingStarted") lifecycleRegistry.currentState = Lifecycle.State.STARTED } override fun onDreamingStopped() { super.onDreamingStopped() + Timber.d("onDreamingStopped") lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt index a1754298..1706ba52 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt @@ -1,6 +1,5 @@ package com.github.damontecres.wholphin.services -import androidx.navigation3.runtime.NavKey import com.github.damontecres.wholphin.ui.nav.Destination import org.acra.ACRA import timber.log.Timber @@ -14,7 +13,7 @@ import javax.inject.Singleton class NavigationManager @Inject constructor() { - var backStack: MutableList = mutableListOf() + var backStack: MutableList = mutableListOf() /** * Go to the specified [com.github.damontecres.wholphin.ui.nav.Destination] 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 4f8e4366..aea22bf1 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 @@ -132,6 +132,14 @@ fun SeriesDetails( var showPlaylistDialog by remember { mutableStateOf>(Optional.absent()) } var showDeleteDialog by remember { mutableStateOf(null) } + LifecycleResumeEffect(destination.itemId) { + viewModel.refresh() + + onPauseOrDispose { + viewModel.release() + } + } + when (val state = loading) { is LoadingState.Error -> { ErrorMessage(state, modifier) @@ -148,9 +156,7 @@ fun SeriesDetails( LifecycleResumeEffect(destination.itemId) { viewModel.onResumePage() - onPauseOrDispose { - viewModel.release() - } + onPauseOrDispose {} } val played = item.data.userData?.played ?: false 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 2d044670..cfc43be9 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 @@ -271,9 +271,9 @@ class SeriesViewModel } fun onResumePage() { - viewModelScope.launchIO { - item.value?.let { - backdropService.submit(it) + item.value?.let { item -> + viewModelScope.launchDefault { backdropService.submit(item) } + viewModelScope.launchIO { val playThemeSongs = userPreferencesService .getCurrent() @@ -283,6 +283,17 @@ class SeriesViewModel } } + fun refresh() { + item.value?.let { item -> + if (loading.value == LoadingState.Success) { + viewModelScope.launchIO { + val seasons = getSeasons(item, null).await() + this@SeriesViewModel.seasons.setValueOnMain(seasons) + } + } + } + } + fun release() { themeSongPlayer.stop() } @@ -292,6 +303,7 @@ class SeriesViewModel seasonNum: Int?, ): Deferred> = viewModelScope.async(Dispatchers.IO) { + Timber.v("getSeasons for %s", series.id) val request = GetItemsRequest( parentId = series.id, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt index 302a734e..0cf42622 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt @@ -4,18 +4,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.runtime.serialization.NavBackStackSerializer -import androidx.navigation3.runtime.serialization.NavKeySerializer import androidx.navigation3.ui.NavDisplay import androidx.tv.material3.DrawerValue import androidx.tv.material3.rememberDrawerState @@ -53,22 +48,12 @@ class ApplicationContentViewModel fun ApplicationContent( server: JellyfinServer, user: JellyfinUser, - startDestination: Destination, navigationManager: NavigationManager, preferences: UserPreferences, modifier: Modifier = Modifier, enableTopScrim: Boolean = true, viewModel: ApplicationContentViewModel = hiltViewModel(), ) { - val backStack: MutableList = - rememberSerializable( - server, - user, - serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()), - ) { - NavBackStack(startDestination) - } - navigationManager.backStack = backStack val drawerState = rememberDrawerState(DrawerValue.Closed) Box( modifier = modifier, diff --git a/scripts/ffmpeg/README.md b/scripts/ffmpeg/README.md index d1fdac02..36a9ca9a 100644 --- a/scripts/ffmpeg/README.md +++ b/scripts/ffmpeg/README.md @@ -1,3 +1,14 @@ # FFmpeg ExoPlayer decoder extension module Builds the ffmpeg decoder extension module for `ExoPlayer` which supports extra codecs during playback. + +## Usage + +```sh +./build_ffmpeg_decoder.sh /path/to/android/ndk +``` + +Or clean build: +```sh +./build_ffmpeg_decoder.sh /path/to/android/ndk --clean +``` diff --git a/scripts/ffmpeg/build_ffmpeg_decoder.sh b/scripts/ffmpeg/build_ffmpeg_decoder.sh index 1741c703..338b850e 100755 --- a/scripts/ffmpeg/build_ffmpeg_decoder.sh +++ b/scripts/ffmpeg/build_ffmpeg_decoder.sh @@ -27,6 +27,12 @@ AV1_MODULE_PATH="$MEDIA_PATH/libraries/decoder_av1/src/main" HOST="$(uname -s | tr '[:upper:]' '[:lower:]')" HOST_PLATFORM="$HOST-x86_64" +if [[ "$2" == "--clean" ]]; then + rm -rf ffmpeg_decoder + rm -f "$TARGET_PATH/lib-decoder-ffmpeg-release.aar" + rm -f "$TARGET_PATH/lib-decoder-av1-release.aar" +fi + mkdir -p "$TARGET_PATH" mkdir -p ffmpeg_decoder @@ -38,14 +44,16 @@ pushd ffmpeg_decoder || exit if [[ -d media ]]; then pushd media || exit - git checkout --force "$media_version" + git fetch origin "$media_version" --depth 1 + git checkout --force FETCH_HEAD else git clone https://github.com/androidx/media.git --depth 1 --single-branch -b "$media_version" media fi if [[ -d ffmpeg ]]; then pushd ffmpeg || exit - git checkout --force "$FFMPEG_BRANCH" + git fetch origin "$FFMPEG_BRANCH" --depth 1 + git checkout --force FETCH_HEAD else git clone https://github.com/FFmpeg/FFmpeg --depth 1 --single-branch -b "$FFMPEG_BRANCH" ffmpeg fi @@ -68,7 +76,8 @@ pushd "$AV1_MODULE_PATH/jni" || exit if [[ -d dav1d ]]; then pushd dav1d || exit - git checkout --force "$DAV1D_BRANCH" + git fetch origin "$DAV1D_BRANCH" --depth 1 + git checkout --force FETCH_HEAD else git clone https://code.videolan.org/videolan/dav1d --depth 1 --single-branch -b "$DAV1D_BRANCH" dav1d fi diff --git a/scripts/mpv/get_dependencies.sh b/scripts/mpv/get_dependencies.sh index 74bff6d6..829b2a06 100755 --- a/scripts/mpv/get_dependencies.sh +++ b/scripts/mpv/get_dependencies.sh @@ -15,7 +15,8 @@ function clone(){ if [[ -d "$dir" ]]; then pushd "$dir" || exit - git checkout --force "$branch" + git fetch origin "$branch" --depth 1 + git checkout --force FETCH_HEAD popd || exit else git clone "$repo" --depth 1 --single-branch -b "$branch" "$dir" "$@"