Merge branch 'main' into fea/jellyseerr

This commit is contained in:
Damontecres 2025-12-08 13:26:25 -05:00
commit 2c6ecc24ea
No known key found for this signature in database
6 changed files with 79 additions and 58 deletions

View file

@ -18,23 +18,34 @@ This is not a fork of the [official client](https://github.com/jellyfin/jellyfin
</a> </a>
</p> </p>
<img width="1280" height="771" alt="0 3 0_home" src="https://github.com/user-attachments/assets/23be4786-a4c3-45e5-a710-8d1ade2be2a3" /> <img width="1280" height="720" alt="0_3_5_home" src="https://github.com/user-attachments/assets/a485c015-ec21-442d-a757-1f18381bf799" />
## Features ## Features
### User interface
- A navigation drawer for quick access to libraries, search, and settings from almost anywhere in the app - A navigation drawer for quick access to libraries, search, and settings from almost anywhere in the app
- Option to combine Continue Watching & Next Up rows
- Show Movie/TV Show titles when browsing libraries - Show Movie/TV Show titles when browsing libraries
- Play TV Show theme music, if available - Play theme music, if available
- Plex inspired playback controls, such as: - Customize layout grids for libraries
- Using D-Pad left/right for seeking during playback - Access all your favorites quickly from the nav drawer
- Quickly access video chapters during playback - Multiple app color themes
- Optionally skip back a few seconds when resuming playback
- Other (subjective) enhancements: ### Playback
- Subtly show playback position along the bottom of the screen while seeking w/ D-Pad
- Force Continue Watching & Next Up TV episodes to use their Series posters
- Different media playback engines, including: - Different media playback engines, including:
- Default ExoPlayer/Media3 - Default ExoPlayer/Media3
- Experimental MPV - Experimental MPV
- Plex inspired playback controls, such as:
- Using D-Pad left/right for seeking during playback
- Quickly access video chapters & queue during playback
- Optionally skip back a few seconds when resuming playback
- Live TV & DVR support
- Auto play next episodes with pass out protection
- Other (subjective) enhancements:
- Subtly show playback position along the bottom of the screen while seeking w/ D-Pad
- Force Continue Watching & Next Up TV episodes to use their Series posters
### Roadmap ### Roadmap
@ -88,8 +99,11 @@ You can [help translate Wholphin](https://translate.codeberg.org/engage/wholphin
### Movie library browsing ### Movie library browsing
<img width="1280" height="771" alt="0 3 0_movies" src="https://github.com/user-attachments/assets/a49829b5-bc2c-4af9-8d5d-2f7d0973ce01" /> <img width="1280" height="771" alt="0 3 0_movies" src="https://github.com/user-attachments/assets/a49829b5-bc2c-4af9-8d5d-2f7d0973ce01" />
### Movie page
<img width="1280" height="720" alt="0_3_5_movie" src="https://github.com/user-attachments/assets/86af5889-6761-426a-8649-422f9d0a1dc0" />
### Series page ### Series page
<img width="1280" height="771" alt="0 3 0_series_overview" src="https://github.com/user-attachments/assets/2d26f34e-3ee8-4599-ba59-32524637dab7" /> <img width="1280" height="720" alt="0_3_5_series" src="https://github.com/user-attachments/assets/2dcb2260-53ce-49d6-9088-72cbd4563c48" />
### Playlist ### Playlist
<img width="1280" height="771" alt="0 3 0_playlist" src="https://github.com/user-attachments/assets/7ca589ab-9c88-483a-b769-35ffb5663d9e" /> <img width="1280" height="771" alt="0 3 0_playlist" src="https://github.com/user-attachments/assets/7ca589ab-9c88-483a-b769-35ffb5663d9e" />

View file

@ -1,7 +1,6 @@
package com.github.damontecres.wholphin package com.github.damontecres.wholphin
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -10,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -45,7 +43,6 @@ import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.ui.CoilConfig import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
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.nav.Destination
@ -53,8 +50,6 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.util.DebugLogTree import com.github.damontecres.wholphin.util.DebugLogTree
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -146,16 +141,17 @@ class MainActivity : AppCompatActivity() {
) { ) {
var isRestoringSession by remember { mutableStateOf(true) } var isRestoringSession by remember { mutableStateOf(true) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (appPreferences.signInAutomatically) { // TODO PIN-related
try { // if (appPreferences.signInAutomatically) {
serverRepository.restoreSession( try {
appPreferences.currentServerId?.toUUIDOrNull(), serverRepository.restoreSession(
appPreferences.currentUserId?.toUUIDOrNull(), appPreferences.currentServerId?.toUUIDOrNull(),
) appPreferences.currentUserId?.toUUIDOrNull(),
} catch (ex: Exception) { )
Timber.e(ex, "Exception restoring session") } catch (ex: Exception) {
} Timber.e(ex, "Exception restoring session")
} }
// }
isRestoringSession = false isRestoringSession = false
} }
val current by serverRepository.current.observeAsState() val current by serverRepository.current.observeAsState()
@ -177,32 +173,34 @@ class MainActivity : AppCompatActivity() {
) )
} }
} else { } else {
DisposableEffect(Unit) { // TODO PIN-related
onDispose { // DisposableEffect(Unit) {
if (!appPreferences.signInAutomatically) { // onDispose {
serverRepository.closeSession() // if (!appPreferences.signInAutomatically || current?.user?.hasPin == true) {
} // serverRepository.closeSession()
} // }
} // }
// }
key(current?.server?.id, current?.user?.id) { key(current?.server?.id, current?.user?.id) {
LaunchedEffect(current?.user?.pin) { // TODO PIN-related
if (current?.user?.pin?.isNotNullOrBlank() == true) { // LaunchedEffect(current?.user?.pin) {
// If user has a pin, then obscure the window in previews // if (current?.user?.pin?.isNotNullOrBlank() == true) {
window?.setFlags( // // If user has a pin, then obscure the window in previews
WindowManager.LayoutParams.FLAG_SECURE, // window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, // WindowManager.LayoutParams.FLAG_SECURE,
) // WindowManager.LayoutParams.FLAG_SECURE,
} else { // )
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) // } else {
} // window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
} // }
// }
val initialDestination = val initialDestination =
when { when {
current != null -> Destination.Home() current != null -> Destination.Home()
!appPreferences.signInAutomatically -> Destination.ServerList // TODO PIN-related
// !appPreferences.signInAutomatically -> Destination.ServerList
// TODO user list?
else -> Destination.ServerList else -> Destination.ServerList
} }
val backStack = rememberNavBackStack(initialDestination) val backStack = rememberNavBackStack(initialDestination)
@ -249,11 +247,12 @@ class MainActivity : AppCompatActivity() {
override fun onRestart() { override fun onRestart() {
super.onRestart() super.onRestart()
val signInAutomatically = // TODO PIN-related
runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true // val signInAutomatically =
Timber.i("onRestart: signInAutomatically=$signInAutomatically") // runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true
if (!signInAutomatically) { // Timber.i("onRestart: signInAutomatically=$signInAutomatically")
serverRepository.closeSession() // if (!signInAutomatically || serverRepository.currentUser.value?.hasPin == true) {
} // serverRepository.closeSession()
// }
} }
} }

View file

@ -109,6 +109,8 @@ class ServerRepository
/** /**
* Restores a session for the given server & user such as when the app reopens * Restores a session for the given server & user such as when the app reopens
*
* If user has a PIN, this returns false
*/ */
suspend fun restoreSession( suspend fun restoreSession(
serverId: UUID?, serverId: UUID?,
@ -124,7 +126,7 @@ class ServerRepository
} }
if (serverAndUsers != null) { if (serverAndUsers != null) {
val user = serverAndUsers.users.firstOrNull { it.id == userId } val user = serverAndUsers.users.firstOrNull { it.id == userId }
if (user != null) { if (user != null && !user.hasPin) {
changeUser(serverAndUsers.server, user) changeUser(serverAndUsers.server, user)
return true return true
} }

View file

@ -52,6 +52,8 @@ data class JellyfinUser(
val accessToken: String?, val accessToken: String?,
val pin: String? = null, val pin: String? = null,
) { ) {
val hasPin: Boolean get() = pin.isNotNullOrBlank()
override fun toString(): String = override fun toString(): String =
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})" "JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})"
} }

View file

@ -793,7 +793,8 @@ val basicPreferences =
title = R.string.ui_interface, title = R.string.ui_interface,
preferences = preferences =
listOf( listOf(
AppPreference.SignInAuto, // TODO PIN-related
// AppPreference.SignInAuto,
AppPreference.HomePageItems, AppPreference.HomePageItems,
AppPreference.CombineContinueNext, AppPreference.CombineContinueNext,
AppPreference.RewatchNextUp, AppPreference.RewatchNextUp,
@ -826,7 +827,8 @@ val basicPreferences =
title = R.string.profile_specific_settings, title = R.string.profile_specific_settings,
preferences = preferences =
listOf( listOf(
AppPreference.RequireProfilePin, // TODO PIN-related
// AppPreference.RequireProfilePin,
AppPreference.UserPinnedNavDrawerItems, AppPreference.UserPinnedNavDrawerItems,
), ),
), ),

View file

@ -119,11 +119,13 @@ fun SwitchUserContent(
users = users, users = users,
currentUser = currentUser, currentUser = currentUser,
onSwitchUser = { user -> onSwitchUser = { user ->
if (user.pin.isNotNullOrBlank()) { // TODO PIN-related
switchUserWithPin = user // if (user.pin.isNotNullOrBlank()) {
} else { // switchUserWithPin = user
viewModel.switchUser(user) // } else {
} // viewModel.switchUser(user)
// }
viewModel.switchUser(user)
}, },
onAddUser = { showAddUser = true }, onAddUser = { showAddUser = true },
onRemoveUser = { user -> onRemoveUser = { user ->