diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 62e3ec054..67b5936c0 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -10,7 +10,7 @@ runs: with: python-version: '3.14' - name: Set up JDK - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: distribution: zulu java-version: '21' @@ -21,7 +21,7 @@ runs: echo "BUILD_TOOLS_VERSION=36.0.0" >> $GITHUB_ENV echo "NDK_VERSION=29.0.14206865" >> $GITHUB_ENV - name: Set up Android SDK - uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4 + uses: android-actions/setup-android@be39fa834029ff78f1a44aa3bb0819b8fc2bd8fd # v4 with: packages: "platform-tools platforms;android-37.0 build-tools;${{ env.BUILD_TOOLS_VERSION }} ndk;${{ env.NDK_VERSION }}" - name: Add NDK tools to PATH diff --git a/Intents.md b/Intents.md index 6e428ab60..af13f29d2 100644 --- a/Intents.md +++ b/Intents.md @@ -63,7 +63,7 @@ Action: `android.intent.action.VIEW` Shorthand action: `view` Parameters: -- `itemId` - Required, the UUID of the media item +- `itemId` - Optional, the UUID of the media item, if not set, goes to home screen ### Examples @@ -135,12 +135,20 @@ adb shell am start -d 'mosaic://play?itemId=5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d Full format ```bash +# Switch user, go to item adb shell am start \ -a android.intent.action.VIEW \ -n 'com.github.damontecres.wholphin/.MainActivity' \ --es itemId "5cf8f8e7-2a5f-4aa9-8c12-ddf63d42ee6d" \ --es serverId "341880f9-ca88-4038-8718-f6b2407950b1" \ --es userId "5c8575e0-468d-44fb-8c70-00aa79160587" + +# Switch user, go to home screen +adb shell am start \ + -a android.intent.action.VIEW \ + -n 'com.github.damontecres.wholphin/.MainActivity' \ + --es serverId "341880f9-ca88-4038-8718-f6b2407950b1" \ + --es userId "5c8575e0-468d-44fb-8c70-00aa79160587" ``` URI based diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index dd1236a49..eefb001b8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -237,21 +237,30 @@ data class BaseItem( // Redirect episodes & seasons to their series if possible when (type) { BaseItemKind.EPISODE -> { - data.seasonId?.let { seasonId -> + val seasonId = data.seasonId + val seriesId = data.seriesId + if (seasonId != null && seriesId != null) { Destination.SeriesOverview( - data.seriesId!!, + seriesId, BaseItemKind.SERIES, SeasonEpisodeIds(seasonId, data.parentIndexNumber, id, indexNumber), ) - } ?: Destination.MediaItem(this) + } else { + Destination.MediaItem(this) + } } BaseItemKind.SEASON -> { - Destination.SeriesOverview( - data.seriesId!!, - BaseItemKind.SERIES, - SeasonEpisodeIds(id, indexNumber, null, null), - ) + val seriesId = data.seriesId + if (seriesId != null) { + Destination.SeriesOverview( + seriesId, + BaseItemKind.SERIES, + SeasonEpisodeIds(id, indexNumber, null, null), + ) + } else { + Destination.MediaItem(this) + } } BaseItemKind.TV_CHANNEL -> { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index e7b5db000..3371f45eb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -245,7 +245,7 @@ class HomeSettingsService settings.rows.mapIndexed { index, config -> resolve(index, config) } - HomePageResolvedSettings(resolvedRows) + HomePageResolvedSettings(userId, resolvedRows) } else { createDefault(userId) } @@ -256,12 +256,15 @@ class HomeSettingsService /** * Resolve the settings and set them to be the current settings */ - suspend fun updateCurrent(settings: HomePageSettings) { + suspend fun updateCurrent( + userId: UUID, + settings: HomePageSettings, + ) { val resolvedRows = settings.rows.mapIndexed { index, config -> resolve(index, config) } - val resolvedSettings = HomePageResolvedSettings(resolvedRows) + val resolvedSettings = HomePageResolvedSettings(userId, resolvedRows) currentSettings.update { resolvedSettings } } @@ -312,7 +315,7 @@ class HomeSettingsService ), ) val rowConfig = continueWatchingRow + includedIds - return HomePageResolvedSettings(rowConfig) + return HomePageResolvedSettings(userId, rowConfig) } /** @@ -428,7 +431,7 @@ class HomeSettingsService null } }.flatten() - HomePageResolvedSettings(rowConfigs) + HomePageResolvedSettings(userId, rowConfigs) } else { null } @@ -1224,10 +1227,11 @@ data class HomeRowConfigDisplay( * @see HomePageSettings */ data class HomePageResolvedSettings( + val userId: UUID, val rows: List, ) { companion object { - val EMPTY = HomePageResolvedSettings(listOf()) + val EMPTY = HomePageResolvedSettings(UUID.randomUUID(), emptyList()) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt index 68678a6da..88ac02990 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/IntentService.kt @@ -56,30 +56,37 @@ class IntentService val query = intent.getStringParam(SearchManager.QUERY) return IntentResult.Target(listOf(Destination.Search(query ?: ""))) } - val itemId = - intent.getStringParam("itemId")?.toUUIDOrNull() - ?: return IntentResult.Error("No item id provided") + val itemId = intent.getStringParam("itemId")?.toUUIDOrNull() val item = - try { - api.userLibraryApi - .getItem(itemId) - .content - .let { BaseItem(it) } - } catch (ex: Exception) { - Timber.w(ex, "Error fetching item %s", itemId) - return IntentResult.Error("Could not fetch item $itemId") + itemId?.let { + try { + api.userLibraryApi + .getItem(itemId) + .content + .let { BaseItem(it) } + } catch (ex: Exception) { + Timber.w(ex, "Error fetching item %s", itemId) + return IntentResult.Error("Could not fetch item $itemId") + } } - - val itemDestination = item.destination() + val itemDestination = item?.destination() val destinations = when (action) { Intent.ACTION_VIEW, "view" -> { - listOf(itemDestination) + if (itemDestination != null) { + listOf(itemDestination) + } else { + // Will go to the home page + emptyList() + } } "${BuildConfig.APPLICATION_ID}.PLAYBACK", "com.github.damontecres.wholphin.PLAYBACK", "play" -> { + if (itemId == null || itemDestination == null) { + return IntentResult.Error("Cannot start playback with no itemId") + } val position = intent.getLongParam("position")?.coerceAtLeast(0) val shuffle = intent.getBooleanExtra("shuffle", false) @@ -105,7 +112,7 @@ class IntentService } } - return IntentResult.Target(destinations) + return IntentResult.Target(destinations, true) } internal suspend fun prepare(intent: Intent): IntentResult? { @@ -196,6 +203,8 @@ class IntentService const val INTENT_SEASON_ID = "seaId" const val INTENT_SERVER_ID = "serverId" const val INTENT_USER_ID = "userId" + + const val ACTION_PLAYBACK = "com.github.damontecres.wholphin.PLAYBACK" } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt index 09aa29a82..c3fcb4786 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt @@ -173,9 +173,10 @@ class ScreensaverService */ fun keepScreenOn(keep: Boolean) { scope.launchDefault { - val screensaverEnabled = _state.value.run { enabled || dimEnabled } + val screensaverEnabled = state.value.enabled + val dimEnabled = state.value.dimEnabled Timber.d("Keep screen on: %s, screensaverEnabled=%s", keep, screensaverEnabled) - if (screensaverEnabled) { + if (screensaverEnabled || dimEnabled) { // Page is requesting to keep screen on, so we don't wait to show the screensaver _state.update { it.copy( @@ -187,7 +188,9 @@ class ScreensaverService if (!keep) { pulse() } - } else { + } + if (!screensaverEnabled) { + // If in-app screensaver is not enabled, send keep screen on to the OS keepScreenOnInternal(keep) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt index fa42469e4..abe39959e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt @@ -27,9 +27,6 @@ class SetupNavigationManager fun navigateTo(destination: SetupDestination) { backStack[0] = destination log() - if (destination !is SetupDestination.AppContent) { - navigationManager.reloadHome() - } } private fun log() { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt index 8471c40dc..adbd9a44b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt @@ -787,7 +787,9 @@ fun List.possibleFavoriteTypes(sort: Boolean) = flatMap { it.collectionType.baseItemKinds } .distinct() .filter { it in favoriteOptions } - .let { list -> + .let { + // No library for persons, so always include it + val list = it + listOf(BaseItemKind.PERSON) if (sort) { list.sortedWith(compareBy { favoriteOptions.indexOf(it) }) } else { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index 5b74c8d8b..a4d83ee3e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -21,6 +21,8 @@ import com.github.damontecres.wholphin.services.ServerReportService import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.deleteItem import com.github.damontecres.wholphin.services.tvAccess +import com.github.damontecres.wholphin.ui.collectLatestIn +import com.github.damontecres.wholphin.ui.combinePair import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO @@ -32,6 +34,8 @@ import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.WholphinDispatchers import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow @@ -45,6 +49,7 @@ import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.UserDto import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -70,6 +75,8 @@ class HomeViewModel private val _state = MutableStateFlow(HomeState.EMPTY) val state: StateFlow = _state + private var dataLoadingJob: Job? = null + init { datePlayedService.invalidateAll() viewModelScope.launch { @@ -77,148 +84,200 @@ class HomeViewModel _state.update { it.copy(acquiringItems = acquiring.items) } } } -// init() + serverRepository.currentUserDtoFlow + .combinePair(homeSettingsService.currentSettings) + .collectLatestIn(viewModelScope) { (userDto, settings) -> + Timber.v( + "Got new userDto & settings, userId=%s, settings?=%s", + userDto?.id, + settings != HomePageResolvedSettings.EMPTY, + ) + _state.update { HomeState.EMPTY.copy(acquiringItems = it.acquiringItems) } + dataLoadingJob?.cancel() + if (userDto == null) { + Timber.d("UserDto is null") + return@collectLatestIn + } + if (settings == HomePageResolvedSettings.EMPTY) { + Timber.d("Home settings are empty") + return@collectLatestIn + } + if (userDto.id != settings.userId) { + Timber.d("User IDs don't match: %s vs %s", userDto.id, settings.userId) + return@collectLatestIn + } + dataLoadingJob = + viewModelScope.launchIO { + loadHomeRows(userDto, settings) + } + } } fun init() { viewModelScope.launchIO { Timber.d("init HomeViewModel") - try { - val preferences = userPreferencesService.getCurrent() - val prefs = preferences.appPreferences.homePagePreferences - - serverRepository.currentUserDto?.let { userDto -> - val libraries = - navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess) + serverRepository.currentUserDto?.let { userDto -> + if (dataLoadingJob?.isActive == false) { val settings = homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY } - val state = state.value - - // Refreshing if a load has already occurred and the rows haven't significantly changed - val refresh = - state.loadingState == LoadingState.Success && state.settings == settings - Timber.v( - "refresh=%s, state.loadingState=%s, %s rows", - refresh, - state.loadingState, - settings.rows.size, - ) - _state.update { - it.copy( - loadingState = if (refresh) LoadingState.Success else LoadingState.Loading, - refreshState = LoadingState.Loading, - settings = settings, - homeRows = - if (refresh) { - it.homeRows - } else { - List(settings.rows.size) { - HomeRowLoadingState.Pending(EmptyStringProvider) - } - }, + if (userDto.id == settings.userId) { + dataLoadingJob = + viewModelScope.launchIO { + loadHomeRows(userDto, settings) + } + } else { + Timber.d( + "Init user IDs don't match: %s vs %s", + userDto.id, + settings.userId, ) } + } else { + Timber.v("Data loading job is active") + } + } + } + } - val semaphore = Semaphore(4) + suspend fun loadHomeRows( + userDto: UserDto, + settings: HomePageResolvedSettings, + ) { + Timber.i("Starting loadHomeRows") + try { + val preferences = userPreferencesService.getCurrent() + val prefs = preferences.appPreferences.homePagePreferences - val deferred = - settings.rows - .map { row -> - viewModelScope.async(WholphinDispatchers.IO) { - semaphore.withPermit { - Timber.v("Fetching row: %s", row) - try { - homeSettingsService.fetchDataForRow( - row = row.config, - scope = viewModelScope, - prefs = prefs, - userDto = userDto, - libraries = libraries, - limit = prefs.maxItemsPerRow, - isRefresh = refresh, - ) - } catch (ex: InvalidStatusException) { - if (ex.status == 404) { - Timber.w(ex, "404 on row %s", row) - HomeRowLoadingState.Success( - row.title, - emptyList(), - ) - } else { - Timber.e( - ex, - "Error %s on row %s", - ex.status, - row, - ) - HomeRowLoadingState.Error( - row.title, - exception = ex, - ) - } - } catch (ex: Exception) { - Timber.e(ex, "Error on row %s", row) - HomeRowLoadingState.Error( - row.title, - exception = ex, - ) - } - } - } + val libraries = + navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess) + + val state = state.value + + // Refreshing if a load has already occurred and the rows haven't significantly changed + val refresh = + state.loadingState == LoadingState.Success && state.settings == settings + Timber.v( + "refresh=%s, state.loadingState=%s, %s rows", + refresh, + state.loadingState, + settings.rows.size, + ) + _state.update { + it.copy( + loadingState = if (refresh) LoadingState.Success else LoadingState.Loading, + refreshState = LoadingState.Loading, + settings = settings, + homeRows = + if (refresh) { + it.homeRows + } else { + List(settings.rows.size) { + HomeRowLoadingState.Pending(EmptyStringProvider) } + }, + ) + } - if (refresh) { - // Replace rows as they complete - val remaining = deferred.withIndex().toMutableList() - while (remaining.isNotEmpty()) { - val (rowIndex, rowData) = - select { - // "Return" the first remaining that is completed - remaining - .forEach { (rowIndex, deferred) -> - deferred.onAwait { rowIndex to it } - } - } - Timber.v("Got row data index=%s", rowIndex) - remaining.removeIf { it.index == rowIndex } - _state.update { state -> - val newRows = - state.homeRows.toMutableList().apply { - set(rowIndex, rowData) + val semaphore = Semaphore(4) + + val deferred = + settings.rows + .map { row -> + viewModelScope.async(WholphinDispatchers.IO) { + semaphore.withPermit { + Timber.v("Fetching row: %s", row) + try { + homeSettingsService.fetchDataForRow( + row = row.config, + scope = viewModelScope, + prefs = prefs, + userDto = userDto, + libraries = libraries, + limit = prefs.maxItemsPerRow, + isRefresh = refresh, + ) + } catch (ex: InvalidStatusException) { + if (ex.status == 404) { + Timber.w(ex, "404 on row %s", row) + HomeRowLoadingState.Success( + row.title, + emptyList(), + ) + } else { + Timber.e( + ex, + "Error %s on row %s", + ex.status, + row, + ) + HomeRowLoadingState.Error( + row.title, + exception = ex, + ) } - state.copy( - homeRows = newRows, - ) + } catch (ex: Exception) { + Timber.e(ex, "Error on row %s", row) + HomeRowLoadingState.Error( + row.title, + exception = ex, + ) + } } } - _state.update { - it.copy( - loadingState = LoadingState.Success, - refreshState = LoadingState.Success, - ) - } - } else { - val rows = deferred.awaitAll() - Timber.v("Got all rows") - _state.update { - it.copy( - loadingState = LoadingState.Success, - refreshState = LoadingState.Success, - homeRows = rows, - ) + } + + if (refresh) { + // Replace rows as they complete + val remaining = deferred.withIndex().toMutableList() + while (remaining.isNotEmpty()) { + val (rowIndex, rowData) = + select { + // "Return" the first remaining that is completed + remaining + .forEach { (rowIndex, deferred) -> + deferred.onAwait { rowIndex to it } + } } + Timber.v("Got row data index=%s", rowIndex) + remaining.removeIf { it.index == rowIndex } + _state.update { state -> + val newRows = + state.homeRows.toMutableList().apply { + set(rowIndex, rowData) + } + state.copy( + homeRows = newRows, + ) } - Timber.d("Home page load complete") } - } catch (ex: Exception) { - Timber.e(ex, "Exception during home page loading") - if (state.value.loadingState == LoadingState.Success) { - showToast(context, "Error refreshing home: ${ex.localizedMessage}") - _state.update { it.copy(refreshState = LoadingState.Error(ex)) } - } else { - _state.update { - it.copy(loadingState = LoadingState.Error(ex)) - } + _state.update { + it.copy( + loadingState = LoadingState.Success, + refreshState = LoadingState.Success, + ) + } + } else { + val rows = deferred.awaitAll() + Timber.v("Got all rows") + _state.update { + it.copy( + loadingState = LoadingState.Success, + refreshState = LoadingState.Success, + homeRows = rows, + ) + } + } + Timber.d("Home page load complete") + } catch (ex: CancellationException) { + throw ex + } catch (ex: Exception) { + Timber.e(ex, "Exception during home page loading") + if (state.value.loadingState == LoadingState.Success) { + showToast(context, "Error refreshing home: ${ex.localizedMessage}") + _state.update { it.copy(refreshState = LoadingState.Error(ex)) } + } else { + _state.update { + it.copy(loadingState = LoadingState.Error(ex)) } } } @@ -312,7 +371,7 @@ data class HomeState( HomeState( LoadingState.Pending, LoadingState.Pending, - listOf(), + emptyList(), HomePageResolvedSettings.EMPTY, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt index 64c0308ed..e3eafcbb8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt @@ -595,7 +595,7 @@ class HomeSettingsViewModel } if (shouldSave) { homeSettingsService.saveToLocal(user.id, settings) - homeSettingsService.updateCurrent(settings) + homeSettingsService.updateCurrent(user.id, settings) showSaveToast() } else { Timber.d("No changes") @@ -616,7 +616,11 @@ class HomeSettingsViewModel _state.update { update.invoke(it) } - homeSettingsService.currentSettings.update { HomePageResolvedSettings(state.value.rows) } + serverRepository.currentUser?.id?.let { userId -> + homeSettingsService.currentSettings.update { + HomePageResolvedSettings(userId, state.value.rows) + } + } } fun resizeCards(relative: Int) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchPage.kt index e353287db..f20222d96 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchPage.kt @@ -113,7 +113,7 @@ fun SearchPage( val focusRequesters = remember(state.includedSearchableTypes.size) { List(RESULTS_START + state.includedSearchableTypes.size) { FocusRequester() } } - val seerrActive by viewModel.seerrActive.collectAsState(initial = false) + val seerrActive by viewModel.seerrActive.collectAsState() var selectedTab by rememberSaveable(seerrActive, state.discoverEnabled) { mutableIntStateOf(0) } var showViewOptions by rememberSaveable { mutableStateOf(false) } var showFilterTypeDialog by rememberSaveable { mutableStateOf(false) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchViewModel.kt index 81dfa95fd..044178b2b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/search/SearchViewModel.kt @@ -43,9 +43,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jellyfin.sdk.api.client.ApiClient @@ -79,7 +81,12 @@ class SearchViewModel private val navDrawerService: NavDrawerService, ) : ViewModel(), ContextMenuProvider { - val seerrActive = seerrService.active + val seerrActive = + seerrService.active.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) private val _state = MutableStateFlow(SearchState()) val state: StateFlow = _state diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt index c47b86c54..e1a6dd962 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt @@ -132,6 +132,7 @@ class SwitchUserViewModel viewModelScope.async(WholphinDispatchers.IO) { try { val current = serverRepository.changeUser(server, user) + navigationManager.reloadHome() setupNavigationManager.navigateTo(SetupDestination.AppContent(current)) SwitchUserResult.Success } catch (ex: InvalidStatusException) { diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 72c7ab266..9a76ef527 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -707,4 +707,6 @@ Dæmp skærmen efter ventetid Dæmpning (%) Dæmp skærmen + Fotoalbummer + Ingen favoritter fundet! diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2e447e0e3..def327d3e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -704,4 +704,16 @@ Nicht unterstützte Server-Version Audio-Hardware-Offload deaktivieren Das Auslagern der Audio Decodierung zu deaktivieren könnte Wiedergabeprobleme auf einigen Gräten beheben + %1$dh %2$ds + S%1$d F%2$d + S%1$d F%2$d-E%3$d + S%1$02dF%2$02d + S%1$02dF%2$02d-E%3$02d + F%1$d + Staffel %1$d + Bildschirm nach Verzögerung dimmen + Dimmen in Prozent + Bildschirm dimmen + Fotoalben + Keine Favoriten vorhanden! diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a4070ecab..732ffe5da 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -18,20 +18,20 @@ Τα βίντεο Παρασκήνιο - Θεματικά τραγούδια + Θεματικό τραγούδι Θεματικά τραγούδια - Θεματικά βίντεο + Θεματικό βίντεο Θεματικά βίντεο - Επόμενο - Ενεργοποιήστε την επανάληψη παρακολούθησης στα \'Επόμενα\' + Επόμενα + Ενεργοποιήστε την επαναπροβολή στα \'Επόμενα\' Γραμματοσειρά Μέγεθος Κειμένου Χρώμα Κειμένου - Φωτεινότητα κειμένου - Βιβλιοθήκης + Έντονη γραμματοσειρά + Βιβλιοθήκη Σειρές Είδη Συλλογή @@ -43,25 +43,25 @@ Στούντιο SDR Ηθοποιοί και Συνεργείο - Περισσότερα Σαν Αυτό + Παραπλήσια Επεισόδια #ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ %s λήψη %s λήψεις - λήψη… + Γίνεται λήψη… Διαδρομή Όνομα - Δοχείο + Κοντέινερ Μέγεθος Κωδικοποιητής AVC Ρυθμός bit Βάθος bit bit - Εξαναγκασμός - Σχέδιο + Αναγκαστικό + Διάταξη Προεπιλογή Εξωτερικό Προσθήκη χρήστη @@ -69,25 +69,25 @@ Κεφάλαια Διάλεξε %1$s Προσθήκη διακομιστή - Ακύρωση καταχώρησης - Ακύρωση σειρά καταχώρησης + Ακύρωση καταγραφής + Ακύρωση καταγραφής σειράς Καθαρισμός μνήμης εικόνας Διαφήμιση Αξιολόγηση κοινού Ένα σφάλμα εμφανίστηκε! Πατήστε το κομπί για να στείλετε αρχείο καταγραφής στο διακομιστή. Επικύρωση - Συνεχίστε να βλέπετε + Συνέχεια προβολής Κριτική - %.1f δευτερόλεπτα + %.2f δευτερόλεπτα Διαγραφή Πέθανε Σκηνοθετήθηκε από %1$s - Απενεργοποιημένος - Ενεργοποιημένος - Καταχώρηση διακομιστή IP or URL + Απενεργοποιημένο + Ενεργοποιημένο + Δώσε την IP ή το URL του διακομιστή Σφάλμα φόρτωσης συλλογής %1$s - Πηγαίνετε στις σειρές - Πηγαίνετε στο + Πήγαινε στη σειρά + Πήγαινε σε αυτό Απόκρυψη χειρισμού αναπαραγωγής Απόκρυψη πληροφοριών εντοπισμού σφαλμάτων Απόκρυψη @@ -96,15 +96,15 @@ Πληροφορίες άδειας Ζωντανή Τηλεόραση Φόρτωση… - Επισήμανση ολόκληρης σειράς ως αναπαραγμένης? - Επισήμανση ολόκληρης σειράς ως μη αναπαραγμένης? + Επισήμανση ολόκληρης σειράς ως αναπαραγμένης; + Επισήμανση ολόκληρης σειράς ως μη αναπαραγμένης; Επισήμανση ως μη ολοκληρωμένο Επισήμανση ως ολοκληρωμένο Μέγιστος ρυθμός μετάδοσης δεδομένων Περισσότερα Χωρίς δεδομένα Χωρίς αποτελέσματα - Χωρίς προγραμματισμένες καταχωρήσεις + Χωρίς προγραμματισμένες καταγραφές Χωρίς διαθέσιμη ενημέρωση Έναρξη από εδώ Έναρξη @@ -117,16 +117,16 @@ Ρυθμίσεις προφίλ χρήστη Ουρά Ανακεφαλαίωση - Πρόσφατα πρόσθετα σε %1$s - Πρόσφατα πρόσθετα + Πρόσφατες προσθήκες σε %1$s + Πρόσφατες προσθήκες Πρόσφατα καταγεγραμμένο - Πρόσφατα κυκλοφόρησε + Πρόσφατης κυκλοφορίας Προτεινόμενο - Πρόγραμμα καταγραφής - Σειρά καταγραφής - Κατάργηση από τα αγαπημένα + Καταγραφή προγράμματος + Καταγραφή σειράς + Αναίρεση αγαπημένου Επανεκκίνηση - Περίληψη + Συνέχισε Αποθήκευση Αναζήτηση Αναζήτηση… @@ -136,9 +136,9 @@ Εμφάνιση επόμενου Εμφάνιση Τυχαία αναπαραγωγή - Αναπήδηση - Προσθήκη ημερομηνίας - Προσθήκη ημερομηνίας επεισοδίου + Παράλειψε + Ημερομηνία Προσθήκης + Ημερομηνία Προσθήκης Επεισοδίου Ημερομηνία αναπαραγωγής Ημερομηνία κυκλοφορίας Όνομα @@ -146,17 +146,17 @@ Υποβολή Ενεργές καταγραφές Κανένας - Εμφάνιση πληροφοριών αναπαραγωγής σφαλμάτων - Λήψη αρχείων διαρκεί πολύ χρονο, ίσως χρειαστεί να επανεκκινήσετε την αναπαραγωγή + Εμφάνιση πληροφοριών σφαλμάτων αναπαραγωγής + Καθυστέρηση της λήψης, ίσως απαιτείται επανεκκίνηση αναπαραγωγής Αλλαγή διακομιστή - Αντικατάσταση - Κορυφαίες βαθμολογίες σε ταινίες που δεν έχω δει + Αλλαγή + Κορυφαίας βαθμολογίας που δεν έχουν προβληθεί Οδηγός Σεζόν Περισσότερες Σεζόν Άγνωστο Αναβαθμίσεις - Εκδοχή + Έκδοση Κλίμακα βίντεο Ζωντανή μετάδοση Πληροφορίες μέσων @@ -167,46 +167,227 @@ Πλάγια γραμματοσειρά Επιτυχής Γονεική αξιολόγηση - Χρόνος εκτέλεσης + Διάρκεια Επιπρόσθετα Άλλο - Άλλο + Άλλα - Παρασκήνια + Παρασκήνιο Παρασκήνια - Αποσπάσματα + Απόσπασμα Αποσπάσματα - Διαγραμμένες σκηνές + Διαγραμμένη σκηνή Διαγραμμένες σκηνές - Συνεντεύξεις + Συνέντευξη Συνεντεύξεις - Σκηνές + Σκηνή Σκηνές - Δείγματα + Δείγμα Δείγματα - Χαρακτηριστικά - Χαρακτηριστικά + Προωθητικό βίντεο + Προωθητικά βίντεο Αυτόματος έλεγχος για ενημερώσεις Καθυστέρηση πριν την έναρξη του επόμενου Αυτόματη έναρξη επόμενου Έλεχγος για ενημερώσεις Ισχύει μόνο για τηλεοπτικές σειρές - Προεπιλεγμένη αναπαραγωγή περιεχομένου + Προεπιλεγμένη κλίμακα περιεχομένου Εγκατάσταση ενημέρωσης - Εγκατεστημένη εκδοχή + Εγκατεστημένη έκδοση + Πληροφορίες + %1$dω %2$dλ %3$dδ + %1$dω %2$dλ + %1$dλ %2$dδ + %1$dω + %1$dλ + %1$dδ + %1$s απέμεινε + %1$dω %2$dδ + Εντοπισμένοι διακομιστές + Τελειώνει σε %1$s + Εναλλαγή ανάλυσης + Τοπικό + Αναπαραγωγή τρέιλερ + Λείπει τρέιλερ + Καθαρισμός επιλογών καναλιού + Δώσε την διεύθυνση διακομιστή + Δεν βρέθηκαν διακομιστές + Μετρητής προβολών + Μόνο αναγκαστικοί υπότιτλοι + Τίτλοι τέλους + Σφάλμα καταγραφής ήχου + Η οπτικοποίηση του ήχου αναπαραγωγής απαιτεί άδεια καταγραφής ήχου. + Καταγραφές + Ρυθμίσεις του προφίλ χρήστη στο διακομιστή + Λειτουργία υποτίτλων + Χρήση των ρυθμίσεων του προφίλ + Προεπιλογή + Έξυπνο + Μόνο αναγκαστικοί + Πάντα + Κανένα + Προτιμώμενη γλώσσα υποτίτλων + Προτιμώμενη γλώσσα ήχου + Περισσότερες ρυθμίσεις του προφίλ χρήστη + Παράκαμψη των ρυθμίσεων του προφίλ χρήστη στον διακομιστή + Οποιαδήποτε γλώσσα + Λειτουργία σινεμά + + + Ταινία + Ταινίες + + + Άτομο + Άτομα + + + + Τρέιλερ + Τρέιλερ + + Προγραμματισμός DVR + + Σειρά + Σειρές + + Διεπαφή + + %1$d έτους + %1$d ετών + + Αναπαραγωγή με μετατροπή + Σειρά τηλεοπτικής προβολής + Παύση/Αναπαραγωγή με το κεντρικό κουμπί του σταυρού πλοήγησης + + Μικρού μήκους + Μικρού μήκους + + + %d ώρα + %d ώρες + + + %d αντικείμενο + %d αντικείμενα + + + %d δευτερόλεπτο + %d δευτερόλεπτα + + Η συσκευή υποστηρίζει AC3/Dolby Digital + Προηγμένη διεπαφή + + Ανώτατο όριο ημερών στα Επόμενα + Στο τέλος της αναπαραγωγής + Κατά τη διάρκεια των τίτλων τέλους + Αφαίρεση σειράς από τα Επόμενα + Εμφάνιση σειρών που αφαιρέθηκαν από τα Επόμενα + Ποτέ + Συμπεριφορά επιλογής του Συνέχεια προβολής/Επόμενο + Αφαίρεση από συνέχεια προβολής + Αναπαραγωγή κατά την επιλογή + Προβολή λεπτομέρειον κατά την επιλογή + Χρήση libass για ASS υπότιτλους + Απευθείας αναπαραγωγή PGS υπότιτλων + Πάντα υποβιβασμός σε στέρεο + Χρήση αποκωδικοποιητή FFmpeg + Ανώτατο όριο αντικειμένων στις γραμμές της αρχικής + Διάλεξε τα προεπιλεγμένα αντικείμενα για εμφάνιση, θα γίνει απόκρυψη των υπόλοιπων + Απομνημόνευση επιλεγμένων καρτελών + Ρυθμίσεις + Ανακάλυψε + Αίτημα + Εκκρεμί + Κωδικός + Όνομα χρήστη + Τάσεις + Κόκκινο + Πράσινο + Μπλε + Θόλωση + Ύψος + Προσαρμογή + Περικοπή + Γέμισμα + Χαμηλότερο + Χαμηλό + Μεσαίο + Υψηλό + Τέρμα + Μωβ + Πορτοκαλί + Μαύρο + Αγνόηση + Άσπρο + Κίτρινο + Κυανό + Πορφυρώ + Περίγραμμα + Σκιά + Αναδίπλωση + Πλαίσιο + Διάρκεια + Προστασία οθόνης + Φωτογραφίες + Ηθοποιός + Συνθέτης + Συγγραφέας + Παραγωγός + Μαέστρος + Στιχουργός + Ενορχηστρωτής + Υπεύθυνος μίξης + Δημιουργός + Καλλιτέχνης + Άλμπουμ + Καλλιτέχνες + Τραγούδια + Άλμπουμ + Στίχοι + Συνεχεία + Μετατροπή + Επαναφορά + Παίχτηκε + Φίλτρο + Έτος + Δεκαετία + Αφαίρεση + Περιθώριο + Λανθασμένο + Στήλες + Διάστιχο + Αυτόματα + Σύνδεση + Γενικά + Τίτλος + Επίπεδο + Ανάλυση + Αναμορφικό + Πεπλεγμένο + Ρυθμός ανανέωσης + Επανάληψη + Επανάλαβε + Φωτεινότητα + Αντίθεση + Κορεσμός + Απόχρωση + Καθυστέρηση υποτίτλων + Ανακάλυψε ταινίες + Ανακάλυψε σειρές diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 417bac6b7..6f57641a1 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -36,4 +36,20 @@ Skip behaviour Customise which to display Continue watching/Next up click behaviour + About + Active Recordings + Add Server + Add User + Audio + Birthplace + Bitrate + Born + Cancel Recording + Cancel Series Recording + %1$dh %2$ds + %1$dm %2$ds + %1$dh + Cancel + Chapters + Choose %1$s diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5eee727c4..cb3513a80 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -726,4 +726,16 @@ Aparece en Desactivar el procesamiento de audio por hardware Desactivar el procesamiento de audio por hardware puede solucionar los bloqueos de reproducción en algunos dispositivos + Atenuar la pantalla tras un tiempo de inactividad + Porcentaje de atenuación + Atenuar pantalla + Álbumes de fotos + ¡No se encontraron favoritos! + %1$dh %2$ds + T%1$d E%2$d + T%1$d E%2$d-E%3$d + T%1$02dE%2$02d + T%1$02dE%2$02d-E%3$02d + E%1$d + Temporada %1$d diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 74cc50555..2e3548f8d 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -704,4 +704,17 @@ On osalenud siin: Lülita graafikaprotsessori kasutamine heli töötluseks välja Kui lülita graafikaprotsessori kasutamise heli töötluseks välja, siis see võib mõnede seadmete puhul parandada taasesituse hangumise vead + Hämarda ekraani peale viivitust + Hämardamise protsent + Hämarda ekraani + Fotoalbumid + Ühtegi lemmikut ei leidu! + %1$d t %2$d sek + S%1$d E%2$d + S%1$d E%2$d-E%3$d + S%1$02dE%2$02d + S%1$02dE%2$02d-E%3$02d + E%1$d + %1$d. hooaeg + Eemalda esitusloendist diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 45442137c..e01773c79 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -729,4 +729,13 @@ Zatamni zaslon nakon odgode Postotak zatamnjenja Zatamni zaslon + %1$dh %2$ds + S%1$d E%2$d + S%1$d E%2$d-E%3$d + S%1$02dE%2$02d + S%1$02dE%2$02d-E%3$02d + E%1$d + Sezona %1$d + Albumi slika + Nijedan favorit nije pronađen! diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 573230c37..b6f315ddd 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -664,4 +664,34 @@ Semua bahasa Cokelat Mode bilah geser D-Pad + %1$dj %2$dd + S%1$d E%2$d + S%1$d E%2$d-E%3$d + S%1$02dE%2$02d + S%1$02dE%2$02d-E%3$02d + E%1$d + Season %1$d + Nonaktifkan offload audio hardware + Menonaktifkan dekode audio offload dapat mengatasi masalah pemutaran macet pada beberapa perangkat + Minimal + Trickplay + Bilah geser kecil di sepanjang bawah layar (default) + Bilah geser dengan gambar pratinjau trickplay + Tanpa bilah geser + Daftar blokir + Hash sesuai + SDH + Acara TV + Rekaman + Folder + Tampilan pengguna + Buku + Buku audio + Versi server tidak didukung + Tampil pada + Redupkan layar setelah penundaan + Persentase redup + Redupkan layar + Album foto + Favorit tidak ditemukan! diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e973af469..b7d7acaeb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -726,4 +726,19 @@ Appare in Disabilita offload hardware audio Disabilitare l’offload della decodifica audio può risolvere i blocchi di riproduzione su alcuni dispositivi + Scurisci lo schermo dopo un ritardo + Percentuale di oscuramento + Scurisci schermo + Album fotografici + Nessun preferito trovato! + %1$do %2$ds + S%1$d E%2$d + S%1$d E%2$d-E%3$d + S%1$02dE%2$02d + S%1$02dE%2$02d-E%3$02d + E%1$d + Stagione %1$d + Rimuovi dalla playlist + Credenziali scadute, effettua di nuovo l’accesso + Non sei autorizzato ad accedere al server in questo momento diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index c8d4ef228..38df9e364 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -726,4 +726,16 @@ מופיע ב־ השבתת פענוח אודיו בהורדה עשויה לתקן קפיאות בניגון במכשירים מסוימים השבתת פענוח אודיו בהורדה + עמעם תצוגה אחרי השהייה + עוצמת העמעום + עמעם תצוגה + אלבומי תמונות + לא נמצאו מועדפים! + %1$d שע\' %2$d שנ\' + עונה %1$d פרק %2$d + עונה %1$d פרקים %2$d-%3$d + עונה %1$02d פרק %2$02d + עונה %1$02d פרקים %2$02d-%3$02d + פרק %1$d + עונה %1$d diff --git a/app/src/main/res/values-lo/strings.xml b/app/src/main/res/values-lo/strings.xml new file mode 100644 index 000000000..045e125f3 --- /dev/null +++ b/app/src/main/res/values-lo/strings.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 9ddb6c770..9e26efccd 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -23,7 +23,7 @@ Bekreft Fortsett å se Kritikervurdering - %.1f sekunder + %.2f sekunder Regissør Deaktivert Oppdagede servere @@ -120,7 +120,7 @@ Bytt bruker Best vurderte usette - Trailere + Trailer Trailere DVR-plan @@ -137,6 +137,7 @@ Videoer Se direkte + %1$d år gammel %1$d år gammel Spill av med transkoding @@ -163,39 +164,39 @@ Bak kulissene - Temasanger - Temasanger + Kjenningsmelodi + Kjenningsmelodier - Temavideoer - Temavideoer + Kjenningsvideo + Kjenningsvideoer Klipp Klipp - Slettede scener + Slettet scene Slettede scener - Intervjuer + Intervju Intervjuer - Scener + Scene Scener - Smakebiter + Smakebit Smakebiter - Bakomfilmer - Bakomfilmer + Novellefilm + Novellefilmer - Kortfilmer + Kortfilm Kortfilmer @@ -223,7 +224,7 @@ Sjekk etter oppdateringer Gjelder kun TV-serier Slå sammen \'Fortsett å se\' og \'Neste\' - Direkteavspilling av ASS-undertekster + Bruk libass for ASS-undertekster Direkteavspilling av PGS-undertekster Alltid nedmiks til stereo Bruk FFmpeg-dekodermodul @@ -365,4 +366,347 @@ Spill trailer Ingen trailere Fjern valg av spor + %1$dh %2$dm %3$ds + %1$dt %2$dmin + %1$dmin %2$ds + %1$dt + %1$dmin + %1$ds + Bare Tvungne Undertekster + Brukerprofil instillinger fra server + Stemmesøk + Begynner + Snakk for å søke + Behandler + Trykk tilbake for å avbryte + Lydopptaksfeil + Stemmegjenkjenningsfeil + Mikrofonadgang nødvendig + Nettverksfeil + Nettverksavbrudd + Ingen tale gjenkjent + Stemmegjenkjenning opptatt + Tjenerfeil + Ingen tale oppdaget + Ukjent feil + Feil ved oppstart av stemmegjenkjenning + Tidsavbrudd ved stemmegjenkjenning + Prøv igjen + %1$s gjenstår + Trailere + Annet + Bak kulissene + Kjenningsmelodier + Kjenningsvideoer + Klipp + Slettede Scener + Intervju + Scener + Prøver + Novellefilmer + Kortfilmer + Favoritt %s + + %s dag + %s dager + + + %s minutt + %s minutter + + + Film + Filmer + + + Serie + Serier + + + Person + Personer + + Krev innlogging for profil + [0-9] + Deaktiver avlasting av lydmaskinvare + Deaktivering av lyddekodingavlasting kan fikse fryst bilde ved avspilling på noen enheter + Kombinert søkeresultater + Vis alle resultater i én liste + Vis resultater etter kategori + Vis knapp for stemmesøk + Synlig + Skjult + Resultater + DTS:X + DTS:HD + DTS:HD MA + TrueHD + DD + DD+ + DTS + Direct play Dolby Vision Profile 7 + Ignorer kompatibilitetsjekk av enheten + Oppdag + Forespørsel + Venter + Seerr integrering + Fjern Seerr Tjener + Seerr tjener lagt til + Rask Tilkobling + Autoriser annen enhet til å logge på kontoen din + For episoder + Forslag for %1$s + Øk størrelsen for alle kort + Reduser størrelsen for alle kort + Bruk miniatyrbilder + Instillinger lagret + Vis forhåndsinnstillinger + Innebygde forhåndsinnstillinger for å raskt style alle rader + Velg rader og bilder på hjemmesiden + + Bruk AC3 for surround sound lyd + Start etter + Animer + Maks aldersgrense + Ingen maks + For alle aldre + Opp til %s år + Skjermsparer + Innstillinger for skjermsparer + Start skjermsparer + Varighet + Bilder + Inkluder typer + Hvilke elementer å vise bilder for + Tilpass + Beskjær + Fyll + Fyll bredde + Fyll høyde + Wholphin Standard + Wholphin Kompakt + Serieminiatyrbilder + Episodeminiatyrbilder + Lavest + Lav + Middels + Høy + Full + Lilla + Oransje + Dristig Blå + Svart + Brun + Ignorer + Hopp over automatisk + Spør om å hoppe over + Bare bruk FFmpeg dersom ingen innebygd dekoder eksisterer + Foretrekk å bruke FFmpeg over innebygde dekodere + Aldri bruk FFmpeg dekodere + Ved slutten av avspilling + Under rulletekst/avslutning + Aldri + Hvit + Lysegrå + Mørkegrå + Gul + Cyan + Magenta + Kant + Skygge + Innpakning + Innebokset + Foretrekk MPV + Ekstern Spiller + Bruk ExoPlayer for avspilling av HDR + Plakat (2:3) + 16:9 + 4:3 + Kvadrat (1:1) + Primær + Miniatyr + Rutenett + Liste + Kompakt Liste + Bilde med dynamisk farge + Bare bilde + + API-nøkkel + Logg inn på Seerr-tjener + Jellyfinbruker + Lokal bruker + + Ingen eksterne undertekster ble funnet + Nåværende + Bruk appens skjermsparer + Er du sikker på at du vil slette dette elementet? + Vis alternativ for behandling av medier + Søk %s + Skuespiller + Komponist + Forfatter + Gjesteartist + Produsent + Dirigent + Sangtekstforfatter + Arrangør + Ingeniør + Lydmikser + Skaper + Artist + Albumartist + Album + Populære sanger + Spill av neste + Musikkvideoer + Sangtekst + Skjul sangtekst + Vis sangtekst + Låt har sangtekst + Fjern fra kø + Vis albumomslag + Vis visualizer + Vis bakgrunn + Spill av som? + Visualisering av lyden som spilles av, krever tillatelse til å ta opp lyd. + Fortsett + Velg alle + Seperate typer + Foretrekk å vise logoer for titler + Wholphin oppdatert til %s + Fjern fra fortsett å se + Fjern serier fra neste + Vis serier fjernet fra neste + Vis mer + Oppdag TV-serier + Oppdag Filmer + Studioer i %1$s + Sist avspilt + System standard + Direkteavspilling med libass + Direkteavspilling med ExoPlayer innebygd + Brenn/transkode på tjener + SSA/ASS avspilling av undertekst + Kinomodus + Vis %1$s mer + Hopp over oppførsel + For introer, rulletekst, etc + Spill med + Transkoding + Språk på brukergrensesnitt + Visning av anmeldelser + Tilpass hva å vise + Profilbeskyttelse + Fortsett å se/Neste klikkoppførsel + Spill av ved klikk + Vis detaljer ved klikk + Stjerner + Tomater + Sorter + Del inn i separate rader + %1$s Filmer + %1$s TV-Serier + Kvalitetsprofil + Standard + Root-mappe + Populæritet + Produksjonstatus + Bruk annen stil for HDR + Eksperimentelle innstillinger + Aktiver eksperimentelle innstillinger? + Eksperimentelle innstillinger kan være ustabile!\n\nDisse kan fjernes eller endre i hvilken som helst versjon. + Skriv inn Quick Connect kode + Kode må være 6 siffer + Enhet autorisering vellykket + Passord + Brukernavn + URL + Trender + Kommende Filmer + Kommende TV-Serier + Forespør i 4K + AV1 programvaredekoding + MPV er nå standardspiller utenom for HDR.\nDu kan endre dette i innstillinger. + HDR undertekststil + Bildeundertekst ugjennomsiktighet + Lysstyrke + Kontrast + Metning + Tone + Rød + Grønn + Blå + Uklarhet + Lagre for album + Start lysbildefremvisning + Stans lysbildefremvisning + Ved start + Ingen flere bilder + Roter venstre + Roter høyre + Zoom inn + Zoom ut + Lysbildefremvisningens varighet + Spill av video under lysbildefremvisning + Send mediainformasjonslogg til tjener + Ingen grense + Maks dager i Neste + Album + Artister + Sanger + Gå til artist + Spilles nå + Øyeblikkelig miks + Gå til album + Legg til i kø + Legg til rad + Sjangre i %1$s + Nylig utgitt i %1$s + Høyde + Bruk på alle rader + Tilpass hjemmesiden + Hjemmerader + Last inn fra tjeners brukerprofil + Lagre på tjeners brukerprofil + Last inn fra web-client + Bruk seriebilde + Legg til rad for %1$s + Overskriv innstillinger på tjener? + Overskriv lokale innstillinger? + Videotunnelering + D-Pad søkelinjemodus + Minimal + Lurespilling + Liten søkelinje langs bunnen av skjermen (standard) + Søkelinje med lurespilling forhåndsvisningsbilde + Ingen søkelinje + Blokkeringslistet + Utilgjengelig + Undertekstmodus + Din brukerprofilinnstilling + Standard + Smart + Bare Tvunget + Alltid + Ingen + Foretrukket undertekstspråk + Foretrukket lydspråk + Flere brukerprofilinnstillinger + Overskriv brukerprofilinnstillingene dine fra tjeneren + Hvilket som helst språk + Hash-matcher + SDH + TV-program + Opptak + Mapper + Brukervisninger + Bøker + Lydbøker + Tjenerversjon ikke støttet + Opptrer på + Dim skjerm etter forsinking + Dim prosent + Dim skjerm + Fotoalbum + Ingen favoritter funnet! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d5a348967..c8d968685 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -42,8 +42,8 @@ Grootte Geforceerd Genres - Ga naar serie - Ga naar item + Naar seizoenen + Naar inhoud Afspeelbediening verbergen Foutopsporingsinformatie verbergen Verbergen @@ -660,4 +660,6 @@ Scenes [0-9] DTS:X + Meer instellingen gebruikersprofiel + "D-pad zoekbalk" diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 28629d944..7e968044a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -749,4 +749,16 @@ Przyciemnienie ekranu Opóźnienie przyciemniana ekranu Procent przyciemniane + Albumy zdjęć + Nic jeszcze nie polubiono! + Sezon %1$d + Usuń z listy odtwarzania + Sesja wygasła, zaloguj się ponownie + Nie jesteś upoważniony do dostępu do serwera w tym momencie + O%1$d + %1$dh %2$ds + S%1$d O%2$d + S%1$d O%2$d-%3$d + S%1$02d O%2$02d + S%1$02d O%2$02d-%3$02d diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6255f0350..033eb50b2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -655,4 +655,66 @@ %1$ds Configuração de Perfil de Usuário do servidor %1$s left + Conteúdos extras + Curtos + + Trailer + Trailers + Trailers + + Exigir login para o perfil + 0-9 + Desativar aceleração de hardware de áudio + Desativar a aceleração de hardware de áudio pode corrigir travamentos durante a reprodução em alguns dispositivos + + Usar AC3 para áudio surround + Marrom + Idioma de exibição + Exibir classificações + Personalizar itens exibidos + Proteção do perfil + Clique em Continuar assistindo/Próximo + Reproduzir ao clicar + Mostrar detalhes ao clicar + Estrelas + Tomates + Ordenar + Dividir em linhas separadas + %1$s Filmes + %1$s Séries + Perfil de qualidade + Padrão + Pasta raiz + Popularidade + Status de produção + Usar estilo diferente para HDR + Configurações experimentais + Ativar configurações experimentais? + Configurações experimentais podem ser instáveis!\n\nElas podem ser removidas ou alteradas em qualquer versão. + Tunelamento de vídeo + Modo da barra de busca com D-Pad + Mínimo + Navegação rápida + Barra de progresso pequena na parte inferior da tela (padrão) + Barra de progresso com imagens de pré-visualização do Trickplay + Sem barra de progresso + Bloqueado + Indisponível + Modo de legenda + Usar configuração do seu perfil + Padrão + Inteligente + Somente forçadas + Sempre + Nenhuma + Idioma preferido das legendas + Idioma preferido do áudio + Mais configurações do perfil + Substituir as configurações do seu perfil definidas pelo servidor + Qualquer idioma + Correspondências de hash + SDH + Programas de TV + Gravações + Pastas diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index d54608d7a..4bc79ea74 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -726,4 +726,16 @@ Aparece em Desativar a processamento de áudio por hardware Desativar o processamento de áudio por hardware pode resolver os bloqueios de reprodução em alguns dispositivos + Escurecimento do ecrã após tempo de inatividade + Percentagem do escurecimento + Escurecer ecrã + Álbum de fotos + Não foram encontrados favoritos! + %1$dh %2$ds + T%1$d E%2$d + T%1$d E%2$d-E%3$d + T%1$02dE%2$02d + T%1$02dE%2$02d-E%3$02d + E%1$d + Temporada %1$d diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e83ae0a83..ba8bb7b3d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -20,7 +20,7 @@ Подтвердить Продолжить просмотр Оценка критиков - %.1f секунда + %.2f секунд По умолчанию Удалить Умер @@ -124,13 +124,15 @@ Продолжительность Дополнительно - Другое + Другой Других Других Других За кадром + За кадром + За кадром За кадром @@ -272,7 +274,7 @@ Автовоспроизведение следующей серии Применяется только к Телешоу Объединить Продолжить просмотр и Далее - Прямое воспроизведение ASS-субтитров + Использовать libaas для ASS-субтитров Прямое воспроизведение PGS-субтитров Размер шрифта Цвет щрифта @@ -294,12 +296,16 @@ %d объектов - Отрывки - Отрывки + Образец + Образца + Образцов + Образцов - Дополнительные материалы - Дополнительные материалы + Дополнительный материал + Дополнительных материала + Дополнительных материалов + Дополнительных материалов Выбор плеера Толщина обводки @@ -307,12 +313,16 @@ Максимальное количество элементов в ряду на домашней странице Форсированные - Саундтреки - Саундтреки + Тематическая песня + Тематические песни + Тематических песен + Тематических песен - Видеоролики - Видеоролики + Тематическое видео + Тематических видео + Тематических видео + Тематических видео Настроить элементы боковой панели Выберите элементы для показа на панели, остальные элементы будут скрыты @@ -326,8 +336,10 @@ Смена страниц по выделению Остановка воспроизведения при бездействии - Короткометражки - Короткометражки + Короткометражка + Короткометражки + Короткометражек + Короткометражек Конец в %1$s Введите адрес сервера @@ -416,4 +428,106 @@ Светло-серый Темно-серый Желтый + + Фильм + Фильма + Фильмов + Фильмов + + + Сериал + Сериала + Сериалов + Сериалов + + + Человек + Человека + Человек + Человек + + + %1$d год + %1$d года + %1$d лет + %1$d лет + + Использовать настройки профиля с сервера + Трейлеры + За кадром + Тематические Песни + Тематические Видео + Клипы + Удаленные Сцены + Интервью + Сцены + Требуется логин для профиля + [0–9] + AVC + SDR + HDR10 + HDR10+ + HLG + Hz + Показать все результаты в одном списке + Показать результаты по категориям + Показать кнопку голосового ввода + DTS:X + DTS:HD + DTS:HD MA + TrueHD + DD + DD+ + DTS + Прямое воспроизведение Dolby Vision Profile 7 + Серверная версия не поддерживается + Избранное %s + Отключение выгрузки аудио-декодирования может исправить фризы воспроизведения на некоторых устройствах + NAL + Показывать названия + Повтор + Сначала показывать избранные каналы + Сортировка каналов по недавно просмотренным + Стиль фона + Комбинированные результаты поиска + Видимый + Скрытый + Результаты + Переключение разрешения + Локальный + Очистить выбор дорожки + Игнорировать проверки совместимости устройства + Обзор + Запрос + В ожидании + Удалить сервер Seerr + Сервер Seerr добавлен + Быстрое Поключение + Авторизовать другое устройство для поключения к вашей учетной записи + Ввести код Быстрого Подключения + В коде должно быть 6 цифр + Устройство авторизовано успешно + URL + В тренде + Ожидаемые сериалы + Ожидаемые фильмы + Запросить в 4K + Программное декодирование AV1 + MPV сейчас плеер по умолчанию исключая HDR.\nВы можете изменить это в настройках. + Воспроизвести слайдшоу + Остановить слайдшоу + Больше нет фото + Повернуть влево + Повернуть вправо + Приблизить + Отдалить + Продолжитьльность слайдшоу + Воспроизводить видео во время слайдшоу + Отправить лог информации о медиа на сервер + Без ограничения + HDR + Размытие + Расположение + Никогда не использовать FFmpeg + Использовать FFmpeg только, если не существует встроенных декодеров diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index c0c2ad092..3d1991188 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -751,4 +751,13 @@ Stlmiť obrazovku po uplynutí času Úroveň stlmenia Stlmenie obrazovky + Fotoalbumy + Nenašli sa žiadne obľúbené položky! + %1$dh %2$ds + S%1$d E%2$d + S%1$d E%2$d-E%3$d + S%1$02dE%2$02d + S%1$02dE%2$02d-E%3$02d + E%1$d + Séria %1$d diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 258a5de20..b5b2954e8 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -346,7 +346,7 @@ Växling av uppdateringsfrekvens Referensbilder Röd - Växling av uppdateringsfrekvens + Växling av upplösning Försök igen Tillåt omvisning i “Nästa upp” API nyckel @@ -648,4 +648,26 @@ Brun Visa visualizer Visa bakgrund + Uppspelning backend + Säsong %1$d + S%1$d E%2$d + S%1$d E%2$d-E%3$d + S%1$02dE%2$02d + %1$dt %2$ds + Oskärpa + S%1$02dE%2$02d-E%3$02d + E%1$d + Visa %1$s till + Standard + Föredraget ljudspråk + Fler profilinställningar + Använd profilinställningar + Ignorera serverns profilinställningar + Serverns version stöds inte + Medverkar i + Fotoalbum + Inga favoriter hittades! + Experimentella inställningar kan vara instabila!\n\n Dessa kan tas bort eller ändras i framtida versioner. + Ta bort från spellista + Trailers diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 951b510c3..2717aa787 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -706,4 +706,13 @@ Belirli bir süre sonra ekranı karart Karartma yüzdesi Ekranı karart + %1$dsa %2$dsn + S%1$d E%2$d + S%1$d E%2$d-E%3$d + S%1$02dE%2$02d + S%1$02dE%2$02d-E%3$02d + E%1$d + Sezon %1$d + Fotoğraf Albümleri + Favori bulunamadı! diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 91dc97bbe..f676b9f1b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -685,4 +685,6 @@ 延迟后调暗屏幕 调暗百分比 调暗屏幕 + 相册 + 未找到收藏! diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f2e45616a..a9f7cc5fb 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -685,4 +685,16 @@ 閒置後調暗螢幕 調暗程度 螢幕調暗 + 未添加任何最愛項目! + 相簿 + %1$d小時%2$d分 + 第%1$d季 第%2$d集 + 第%1$d季 第%2$d-%3$d集 + 第%1$02d季 第%2$02d集 + 第%1$02d季 第%2$02d-%3$02d集 + 第 %1$d 集 + 第 %1$d 季 + 從播放清單中移除 + 您目前無存取此伺服器的權限 + 登入憑證已過期,請重新登入 diff --git a/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt b/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt index bb4904aa3..ee32c7dda 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/services/IntentServiceTest.kt @@ -1,24 +1,34 @@ package com.github.damontecres.wholphin.services +import android.app.SearchManager import android.content.Intent import com.github.damontecres.wholphin.data.CurrentUser +import com.github.damontecres.wholphin.data.JellyfinServerDao 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.preferences.update +import com.github.damontecres.wholphin.test.assertIs import com.github.damontecres.wholphin.test.currentUser +import com.github.damontecres.wholphin.test.movie import com.github.damontecres.wholphin.test.server import com.github.damontecres.wholphin.test.user +import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.successResponse import com.github.damontecres.wholphin.ui.toServerString import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import org.jellyfin.sdk.api.operations.UserLibraryApi import org.jellyfin.sdk.model.UUID +import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before @@ -32,13 +42,16 @@ import org.robolectric.annotation.Config class IntentServiceTest { private val api: ApiClient = mockk() private val serverRepository: ServerRepository = mockk() + private val serverDao: JellyfinServerDao = mockk() private val userPreferencesService: UserPreferencesService = mockk() + private val userLibraryApi: UserLibraryApi = mockk() lateinit var intentService: IntentService private val serverId = UUID.randomUUID() private val userId = UUID.randomUUID() private val currentUser = currentUser(serverId, userId) + private val itemId = UUID.randomUUID() private val protectedCurrentUser = CurrentUser( @@ -49,6 +62,9 @@ class IntentServiceTest { @Before fun setup() { intentService = IntentService(api, serverRepository, userPreferencesService) + every { api.userLibraryApi } returns userLibraryApi + coEvery { userLibraryApi.getItem(itemId) } returns successResponse(movie(itemId)) + every { serverRepository.serverDao } returns serverDao } private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) { @@ -63,16 +79,20 @@ class IntentServiceTest { } } + private fun setupAutoSignInUnprotected() { + setupPreferences { + signInAutomatically = true + currentServerId = serverId.toServerString() + currentUserId = userId.toServerString() + } + every { serverRepository.current } returns MutableStateFlow(currentUser) + coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser + } + @Test fun `Test auto sign in with unprotected profile`() = runTest { - setupPreferences { - signInAutomatically = true - currentServerId = serverId.toServerString() - currentUserId = userId.toServerString() - } - every { serverRepository.current } returns MutableStateFlow(currentUser) - coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser + setupAutoSignInUnprotected() val intent = Intent() val result = intentService.prepare(intent) @@ -221,4 +241,243 @@ class IntentServiceTest { coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } } + + @Test + fun `Test parseIntent with view item`() = + runTest { + setupAutoSignInUnprotected() + + val intent = + Intent().apply { + action = Intent.ACTION_VIEW + putExtra("itemId", itemId.toString()) + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(1, destinations.size) + assertTrue(result.addHomeToBackStack) + val first = destinations.first() + assertIs(first) + + assertEquals(itemId, first.itemId) + + coVerify { userLibraryApi.getItem(itemId) } + } + + @Test + fun `Test parseIntent with view and no itemId`() = + runTest { + setupAutoSignInUnprotected() + + val intent = + Intent().apply { + action = Intent.ACTION_VIEW + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(0, destinations.size) + assertTrue(result.addHomeToBackStack) + + coVerify(exactly = 0) { userLibraryApi.getItem(itemId) } + } + + @Test + fun `Test parseIntent with view and switch user`() = + runTest { + setupAutoSignInUnprotected() + + val newUserId = UUID.randomUUID() + val newServerId = UUID.randomUUID() + val newCurrentUser = currentUser(newServerId, newUserId) + + every { serverRepository.current } returns MutableStateFlow(currentUser) + coEvery { + serverRepository.restoreSession( + newServerId, + newUserId, + ) + } returns newCurrentUser + every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user + + val intent = + Intent().apply { + action = Intent.ACTION_VIEW + putExtra("userId", newUserId.toString()) + putExtra("serverId", newServerId.toString()) + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(0, destinations.size) + assertTrue(result.addHomeToBackStack) + + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } + verify { serverDao.getUser(newServerId, newUserId) } + coVerify { serverRepository.restoreSession(newServerId, newUserId) } + coVerify(exactly = 0) { userLibraryApi.getItem(itemId) } + } + + @Test + fun `Test parseIntent with view, itemId, and switch user`() = + runTest { + setupAutoSignInUnprotected() + + val newUserId = UUID.randomUUID() + val newServerId = UUID.randomUUID() + val newCurrentUser = currentUser(newServerId, newUserId) + + every { serverRepository.current } returns MutableStateFlow(currentUser) + coEvery { + serverRepository.restoreSession( + newServerId, + newUserId, + ) + } returns newCurrentUser + every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user + + val intent = + Intent().apply { + action = Intent.ACTION_VIEW + putExtra("userId", newUserId.toString()) + putExtra("serverId", newServerId.toString()) + putExtra("itemId", itemId.toString()) + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(1, destinations.size) + assertTrue(result.addHomeToBackStack) + val first = destinations.first() + assertIs(first) + + assertEquals(itemId, first.itemId) + + coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) } + verify { serverDao.getUser(newServerId, newUserId) } + coVerify { serverRepository.restoreSession(newServerId, newUserId) } + coVerify { userLibraryApi.getItem(itemId) } + } + + @Test + fun `Test parseIntent with search`() = + runTest { + setupAutoSignInUnprotected() + + val intent = + Intent().apply { + action = Intent.ACTION_SEARCH + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(1, destinations.size) + assertTrue(result.addHomeToBackStack) + val first = destinations.first() + assertIs(first) + + assertEquals("", first.query) + } + + @Test + fun `Test parseIntent with search query`() = + runTest { + setupAutoSignInUnprotected() + + val query = "query123" + val intent = + Intent().apply { + action = Intent.ACTION_SEARCH + putExtra(SearchManager.QUERY, query) + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(1, destinations.size) + assertTrue(result.addHomeToBackStack) + val first = destinations.first() + assertIs(first) + + assertEquals(query, first.query) + } + + @Test + fun `Test parseIntent with play but no itemId`() = + runTest { + setupAutoSignInUnprotected() + + val intent = + Intent().apply { + action = IntentService.ACTION_PLAYBACK + } + val result = intentService.parseIntent(intent) + assertIs(result) + } + + @Test + fun `Test parseIntent with play itemId`() = + runTest { + setupAutoSignInUnprotected() + + val intent = + Intent().apply { + action = IntentService.ACTION_PLAYBACK + putExtra("itemId", itemId.toString()) + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(2, destinations.size) + assertTrue(result.addHomeToBackStack) + val first = destinations.first() + val second = destinations[1] + assertIs(first) + assertIs(second) + + assertEquals(itemId, first.itemId) + assertEquals(itemId, second.itemId) + + coVerify { userLibraryApi.getItem(itemId) } + } + + @Test + fun `Test parseIntent with play itemId and params`() = + runTest { + setupAutoSignInUnprotected() + + val position = 4_000L + val shuffle = true + val intent = + Intent().apply { + action = IntentService.ACTION_PLAYBACK + putExtra("itemId", itemId.toString()) + putExtra("position", position) + putExtra("shuffle", shuffle) + } + val result = intentService.parseIntent(intent) + assertIs(result) + + val destinations = result.destinations + assertEquals(2, destinations.size) + assertTrue(result.addHomeToBackStack) + val first = destinations.first() + val second = destinations[1] + assertIs(first) + assertIs(second) + + assertEquals(itemId, first.itemId) + assertEquals(itemId, second.itemId) + assertEquals(shuffle, second.shuffle) + assertEquals(position, second.positionMs) + + coVerify { userLibraryApi.getItem(itemId) } + } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt index 7f51e994c..34d962da2 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestUtils.kt @@ -4,12 +4,16 @@ import com.github.damontecres.wholphin.data.CurrentUser import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.ibm.icu.impl.Assert import io.mockk.MockKMatcherScope import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.NameGuidPair import org.jellyfin.sdk.model.api.UserDto +import org.junit.Assert.assertNotNull +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract fun MockKMatcherScope.nonBlankString() = match { it.isNotNullOrBlank() } @@ -128,3 +132,20 @@ fun currentUser( server(serverId), user(userId), ) + +/** + * Asserts that the object is not null and is type `T` + * + * Includes a [contract] to smart-cast the object to T + */ +@OptIn(ExperimentalContracts::class) +inline fun assertIs(obj: Any?) { + contract { + returns() implies (obj is T) + } + assertNotNull(obj) + val result = T::class.isInstance(obj) + if (!result) { + Assert.fail("Expected type=${T::class.qualifiedName}, actual type= ${obj!!::class.qualifiedName} ") + } +} diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/main/HomeViewModelTest.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/main/HomeViewModelTest.kt new file mode 100644 index 000000000..6e59a9526 --- /dev/null +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/main/HomeViewModelTest.kt @@ -0,0 +1,119 @@ +package com.github.damontecres.wholphin.ui.main + +import android.content.Context +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.BackdropService +import com.github.damontecres.wholphin.services.DatePlayedService +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.HomePageResolvedSettings +import com.github.damontecres.wholphin.services.HomeSettingsService +import com.github.damontecres.wholphin.services.LatestNextUpService +import com.github.damontecres.wholphin.services.MediaManagementService +import com.github.damontecres.wholphin.services.NavDrawerService +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.ServerReportService +import com.github.damontecres.wholphin.services.UserPreferencesService +import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.WholphinDispatchers +import com.github.damontecres.wholphin.util.configure +import com.github.damontecres.wholphin.util.reset +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.jellyfin.sdk.model.UUID +import org.jellyfin.sdk.model.api.UserDto +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HomeViewModelTest { + private val testDispatcher = StandardTestDispatcher() + + @Before + fun setUp() { + WholphinDispatchers.configure(testDispatcher) + } + + @After + fun tearDown() { + WholphinDispatchers.reset() + } + + @Test + fun `reactive user settings reset preserves acquisition items`() = + runTest(testDispatcher) { + val userId = UUID.randomUUID() + val currentUser = + MutableStateFlow( + UserDto( + id = userId, + name = "test-user", + serverName = "test-server", + hasPassword = true, + hasConfiguredPassword = true, + hasConfiguredEasyPassword = false, + ), + ) + val currentSettings = MutableStateFlow(HomePageResolvedSettings.EMPTY) + val acquiringItem = mockk() + val acquisitionState = MutableStateFlow(HomeAcquiringState(listOf(acquiringItem))) + + val serverRepository = + mockk(relaxed = true) { + every { currentUserDtoFlow } returns currentUser + } + val homeSettingsService = + mockk(relaxed = true) { + every { this@mockk.currentSettings } returns currentSettings + } + val navDrawerService = + mockk(relaxed = true) { + coEvery { getAllUserLibraries(any(), any()) } returns emptyList() + } + val appPreferences = mockk(relaxed = true) + val userPreferencesService = + mockk(relaxed = true) { + coEvery { getCurrent() } returns UserPreferences(appPreferences, null) + } + val acquiringSource = + mockk { + every { state } returns acquisitionState + } + val viewModel = + HomeViewModel( + context = mockk(relaxed = true), + navigationManager = mockk(relaxed = true), + serverRepository = serverRepository, + serverReportService = mockk(relaxed = true), + navDrawerService = navDrawerService, + homeSettingsService = homeSettingsService, + favoriteWatchManager = mockk(relaxed = true), + datePlayedService = mockk(relaxed = true), + backdropService = mockk(relaxed = true), + userPreferencesService = userPreferencesService, + mediaManagementService = mockk(relaxed = true), + latestNextUpService = mockk(relaxed = true), + homeAcquiringSource = acquiringSource, + ) + + advanceUntilIdle() + assertEquals(listOf(acquiringItem), viewModel.state.value.acquiringItems) + + val reloadedSettings = HomePageResolvedSettings(userId, emptyList()) + currentSettings.value = reloadedSettings + advanceUntilIdle() + + assertEquals(reloadedSettings, viewModel.state.value.settings) + assertEquals(LoadingState.Success, viewModel.state.value.loadingState) + assertEquals(listOf(acquiringItem), viewModel.state.value.acquiringItems) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7c6ca928..dc7608cfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ coreKtx = "1.19.0" appcompat = "1.8.0" composeBom = "2026.09.00" mockk = "1.14.11" -robolectric = "4.16.1" +robolectric = "4.17" multiplatformMarkdownRenderer = "0.45.0" okhttpBom = "5.5.0" programguide = "1.6.0"