Merge branch 'main' into fea/music

This commit is contained in:
Damontecres 2026-03-21 12:37:15 -04:00
commit 2b6c4eafb3
No known key found for this signature in database
11 changed files with 88 additions and 40 deletions

View file

@ -9,7 +9,9 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey import androidx.compose.ui.test.pressKey
import androidx.navigation3.runtime.NavBackStack
import com.github.damontecres.wholphin.MainContent 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.ScreensaverService
import com.github.damontecres.wholphin.services.ScreensaverState import com.github.damontecres.wholphin.services.ScreensaverState
import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupDestination
@ -43,16 +45,17 @@ class InstrumentedBasicUiTests {
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
@Test @Test
fun myTest() { fun myTest() {
val navigationManager = NavigationManager()
navigationManager.backStack = NavBackStack(Destination.Home())
// Start the app // Start the app
composeTestRule.setContent { composeTestRule.setContent {
WholphinTheme { WholphinTheme {
MainContent( MainContent(
backStack = mutableListOf(SetupDestination.ServerList), backStack = mutableListOf(SetupDestination.ServerList),
navigationManager = mockk(relaxed = true), navigationManager = navigationManager,
appPreferences = mockk(relaxed = true), appPreferences = mockk(relaxed = true),
backdropService = mockk(relaxed = true), backdropService = mockk(relaxed = true),
screensaverService = screensaverService, screensaverService = screensaverService,
requestedDestination = Destination.Home(),
modifier = Modifier, modifier = Modifier,
) )
} }

View file

@ -24,6 +24,7 @@ import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation3.runtime.NavBackStack
import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.ExperimentalTvMaterial3Api
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreference
@ -67,6 +68,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -127,6 +129,11 @@ class MainActivity : AppCompatActivity() {
private var signInAuto = true private var signInAuto = true
private val json =
Json {
classDiscriminator = "_type"
}
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -134,6 +141,23 @@ class MainActivity : AppCompatActivity() {
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
lifecycle.addObserver(playbackLifecycleObserver) lifecycle.addObserver(playbackLifecycleObserver)
val backStackStr = savedInstanceState?.getString(KEY_BACK_STACK)
if (backStackStr != null) {
Timber.d("Restoring back stack")
var backStack = json.decodeFromString<List<Destination>>(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 -> viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) { if (user?.hasPin == true) {
window?.setFlags( window?.setFlags(
@ -206,17 +230,12 @@ class MainActivity : AppCompatActivity() {
appThemeColors = appPreferences.interfacePreferences.appThemeColors, appThemeColors = appPreferences.interfacePreferences.appThemeColors,
) { ) {
ProvideLocalClock { ProvideLocalClock {
val requestedDestination =
remember(intent) {
intent?.let(::extractDestination) ?: Destination.Home()
}
MainContent( MainContent(
backStack = setupNavigationManager.backStack, backStack = setupNavigationManager.backStack,
navigationManager = navigationManager, navigationManager = navigationManager,
appPreferences = appPreferences, appPreferences = appPreferences,
backdropService = backdropService, backdropService = backdropService,
screensaverService = screensaverService, screensaverService = screensaverService,
requestedDestination = requestedDestination,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }
@ -287,6 +306,8 @@ class MainActivity : AppCompatActivity() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
Timber.d("onSaveInstanceState") Timber.d("onSaveInstanceState")
val str = json.encodeToString(navigationManager.backStack.toList())
outState.putString(KEY_BACK_STACK, str)
} }
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@ -362,6 +383,8 @@ class MainActivity : AppCompatActivity() {
const val INTENT_SEASON_NUMBER = "seaNum" const val INTENT_SEASON_NUMBER = "seaNum"
const val INTENT_SEASON_ID = "seaId" const val INTENT_SEASON_ID = "seaId"
private const val KEY_BACK_STACK = "backStack"
lateinit var instance: MainActivity lateinit var instance: MainActivity
private set private set
} }

View file

@ -33,10 +33,8 @@ import com.github.damontecres.wholphin.services.ScreensaverService
import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.ui.components.AppScreensaver import com.github.damontecres.wholphin.ui.components.AppScreensaver
import com.github.damontecres.wholphin.ui.nav.ApplicationContent 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.SwitchServerContent
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
@Composable @Composable
fun MainContent( fun MainContent(
@ -45,7 +43,6 @@ fun MainContent(
appPreferences: AppPreferences, appPreferences: AppPreferences,
backdropService: BackdropService, backdropService: BackdropService,
screensaverService: ScreensaverService, screensaverService: ScreensaverService,
requestedDestination: Destination,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Surface( Surface(
@ -113,7 +110,6 @@ fun MainContent(
ApplicationContent( ApplicationContent(
user = current.user, user = current.user,
server = current.server, server = current.server,
startDestination = requestedDestination,
navigationManager = navigationManager, navigationManager = navigationManager,
preferences = preferences, preferences = preferences,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),

View file

@ -22,9 +22,7 @@ import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreferences 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.ScreensaverService
import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.ui.components.AppScreensaverContent import com.github.damontecres.wholphin.ui.components.AppScreensaverContent
import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.theme.WholphinTheme 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.collectLatest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -61,6 +60,7 @@ class WholphinDreamService :
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.d("onCreate")
savedStateRegistryController.performRestore(null) savedStateRegistryController.performRestore(null)
lifecycleRegistry.currentState = Lifecycle.State.CREATED lifecycleRegistry.currentState = Lifecycle.State.CREATED
@ -74,6 +74,7 @@ class WholphinDreamService :
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
Timber.d("onAttachedToWindow")
val itemFlow = screensaverService.createItemFlow(lifecycleScope) val itemFlow = screensaverService.createItemFlow(lifecycleScope)
setContentView( setContentView(
ComposeView(this).apply { ComposeView(this).apply {
@ -109,11 +110,13 @@ class WholphinDreamService :
override fun onDreamingStarted() { override fun onDreamingStarted() {
super.onDreamingStarted() super.onDreamingStarted()
Timber.d("onDreamingStarted")
lifecycleRegistry.currentState = Lifecycle.State.STARTED lifecycleRegistry.currentState = Lifecycle.State.STARTED
} }
override fun onDreamingStopped() { override fun onDreamingStopped() {
super.onDreamingStopped() super.onDreamingStopped()
Timber.d("onDreamingStopped")
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
} }
} }

View file

@ -1,6 +1,5 @@
package com.github.damontecres.wholphin.services package com.github.damontecres.wholphin.services
import androidx.navigation3.runtime.NavKey
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import org.acra.ACRA import org.acra.ACRA
import timber.log.Timber import timber.log.Timber
@ -14,7 +13,7 @@ import javax.inject.Singleton
class NavigationManager class NavigationManager
@Inject @Inject
constructor() { constructor() {
var backStack: MutableList<NavKey> = mutableListOf() var backStack: MutableList<Destination> = mutableListOf()
/** /**
* Go to the specified [com.github.damontecres.wholphin.ui.nav.Destination] * Go to the specified [com.github.damontecres.wholphin.ui.nav.Destination]

View file

@ -132,6 +132,14 @@ fun SeriesDetails(
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) } var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) } var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
LifecycleResumeEffect(destination.itemId) {
viewModel.refresh()
onPauseOrDispose {
viewModel.release()
}
}
when (val state = loading) { when (val state = loading) {
is LoadingState.Error -> { is LoadingState.Error -> {
ErrorMessage(state, modifier) ErrorMessage(state, modifier)
@ -148,9 +156,7 @@ fun SeriesDetails(
LifecycleResumeEffect(destination.itemId) { LifecycleResumeEffect(destination.itemId) {
viewModel.onResumePage() viewModel.onResumePage()
onPauseOrDispose { onPauseOrDispose {}
viewModel.release()
}
} }
val played = item.data.userData?.played ?: false val played = item.data.userData?.played ?: false

View file

@ -271,9 +271,9 @@ class SeriesViewModel
} }
fun onResumePage() { fun onResumePage() {
viewModelScope.launchIO { item.value?.let { item ->
item.value?.let { viewModelScope.launchDefault { backdropService.submit(item) }
backdropService.submit(it) viewModelScope.launchIO {
val playThemeSongs = val playThemeSongs =
userPreferencesService userPreferencesService
.getCurrent() .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() { fun release() {
themeSongPlayer.stop() themeSongPlayer.stop()
} }
@ -292,6 +303,7 @@ class SeriesViewModel
seasonNum: Int?, seasonNum: Int?,
): Deferred<List<BaseItem?>> = ): Deferred<List<BaseItem?>> =
viewModelScope.async(Dispatchers.IO) { viewModelScope.async(Dispatchers.IO) {
Timber.v("getSeasons for %s", series.id)
val request = val request =
GetItemsRequest( GetItemsRequest(
parentId = series.id, parentId = series.id,

View file

@ -4,18 +4,13 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavBackStackSerializer
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import androidx.tv.material3.DrawerValue import androidx.tv.material3.DrawerValue
import androidx.tv.material3.rememberDrawerState import androidx.tv.material3.rememberDrawerState
@ -53,22 +48,12 @@ class ApplicationContentViewModel
fun ApplicationContent( fun ApplicationContent(
server: JellyfinServer, server: JellyfinServer,
user: JellyfinUser, user: JellyfinUser,
startDestination: Destination,
navigationManager: NavigationManager, navigationManager: NavigationManager,
preferences: UserPreferences, preferences: UserPreferences,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enableTopScrim: Boolean = true, enableTopScrim: Boolean = true,
viewModel: ApplicationContentViewModel = hiltViewModel(), viewModel: ApplicationContentViewModel = hiltViewModel(),
) { ) {
val backStack: MutableList<NavKey> =
rememberSerializable(
server,
user,
serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()),
) {
NavBackStack(startDestination)
}
navigationManager.backStack = backStack
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
Box( Box(
modifier = modifier, modifier = modifier,

View file

@ -1,3 +1,14 @@
# FFmpeg ExoPlayer decoder extension module # FFmpeg ExoPlayer decoder extension module
Builds the ffmpeg decoder extension module for `ExoPlayer` which supports extra codecs during playback. 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
```

View file

@ -27,6 +27,12 @@ AV1_MODULE_PATH="$MEDIA_PATH/libraries/decoder_av1/src/main"
HOST="$(uname -s | tr '[:upper:]' '[:lower:]')" HOST="$(uname -s | tr '[:upper:]' '[:lower:]')"
HOST_PLATFORM="$HOST-x86_64" 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 "$TARGET_PATH"
mkdir -p ffmpeg_decoder mkdir -p ffmpeg_decoder
@ -38,14 +44,16 @@ pushd ffmpeg_decoder || exit
if [[ -d media ]]; then if [[ -d media ]]; then
pushd media || exit pushd media || exit
git checkout --force "$media_version" git fetch origin "$media_version" --depth 1
git checkout --force FETCH_HEAD
else else
git clone https://github.com/androidx/media.git --depth 1 --single-branch -b "$media_version" media git clone https://github.com/androidx/media.git --depth 1 --single-branch -b "$media_version" media
fi fi
if [[ -d ffmpeg ]]; then if [[ -d ffmpeg ]]; then
pushd ffmpeg || exit pushd ffmpeg || exit
git checkout --force "$FFMPEG_BRANCH" git fetch origin "$FFMPEG_BRANCH" --depth 1
git checkout --force FETCH_HEAD
else else
git clone https://github.com/FFmpeg/FFmpeg --depth 1 --single-branch -b "$FFMPEG_BRANCH" ffmpeg git clone https://github.com/FFmpeg/FFmpeg --depth 1 --single-branch -b "$FFMPEG_BRANCH" ffmpeg
fi fi
@ -68,7 +76,8 @@ pushd "$AV1_MODULE_PATH/jni" || exit
if [[ -d dav1d ]]; then if [[ -d dav1d ]]; then
pushd dav1d || exit pushd dav1d || exit
git checkout --force "$DAV1D_BRANCH" git fetch origin "$DAV1D_BRANCH" --depth 1
git checkout --force FETCH_HEAD
else else
git clone https://code.videolan.org/videolan/dav1d --depth 1 --single-branch -b "$DAV1D_BRANCH" dav1d git clone https://code.videolan.org/videolan/dav1d --depth 1 --single-branch -b "$DAV1D_BRANCH" dav1d
fi fi

View file

@ -15,7 +15,8 @@ function clone(){
if [[ -d "$dir" ]]; then if [[ -d "$dir" ]]; then
pushd "$dir" || exit pushd "$dir" || exit
git checkout --force "$branch" git fetch origin "$branch" --depth 1
git checkout --force FETCH_HEAD
popd || exit popd || exit
else else
git clone "$repo" --depth 1 --single-branch -b "$branch" "$dir" "$@" git clone "$repo" --depth 1 --single-branch -b "$branch" "$dir" "$@"