mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into fea/jellyseerr
This commit is contained in:
commit
a2884a7225
37 changed files with 698 additions and 370 deletions
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin
|
|||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -12,18 +13,19 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Surface
|
||||
|
|
@ -33,24 +35,30 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
|||
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.DeviceProfileService
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
|
||||
import com.github.damontecres.wholphin.services.RefreshRateService
|
||||
import com.github.damontecres.wholphin.services.ServerEventListener
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||
import com.github.damontecres.wholphin.ui.CoilConfig
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
|
||||
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
||||
import com.github.damontecres.wholphin.util.DebugLogTree
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
|
|
@ -59,8 +67,7 @@ import javax.inject.Inject
|
|||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var serverRepository: ServerRepository
|
||||
private val viewModel: MainActivityViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
lateinit var userPreferencesDataStore: DataStore<AppPreferences>
|
||||
|
|
@ -72,27 +79,26 @@ class MainActivity : AppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var navigationManager: NavigationManager
|
||||
|
||||
@Inject
|
||||
lateinit var setupNavigationManager: SetupNavigationManager
|
||||
|
||||
@Inject
|
||||
lateinit var updateChecker: UpdateChecker
|
||||
|
||||
@Inject
|
||||
lateinit var appUpgradeHandler: AppUpgradeHandler
|
||||
|
||||
@Inject
|
||||
lateinit var serverEventListener: ServerEventListener
|
||||
|
||||
@Inject
|
||||
lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver
|
||||
|
||||
@Inject
|
||||
lateinit var deviceProfileService: DeviceProfileService
|
||||
|
||||
@Inject
|
||||
lateinit var imageUrlService: ImageUrlService
|
||||
|
||||
@Inject
|
||||
lateinit var refreshRateService: RefreshRateService
|
||||
|
||||
private var signInAuto = true
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -109,9 +115,13 @@ class MainActivity : AppCompatActivity() {
|
|||
window.attributes = attrs.apply { preferredRefreshRate = mode.refreshRate }
|
||||
}
|
||||
}
|
||||
viewModel.appStart()
|
||||
setContent {
|
||||
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
||||
appPreferences?.let { appPreferences ->
|
||||
LaunchedEffect(appPreferences.signInAutomatically) {
|
||||
signInAuto = appPreferences.signInAutomatically
|
||||
}
|
||||
CoilConfig(
|
||||
diskCacheSizeBytes =
|
||||
appPreferences.advancedPreferences.imageDiskCacheSizeBytes.let {
|
||||
|
|
@ -140,100 +150,85 @@ class MainActivity : AppCompatActivity() {
|
|||
.background(MaterialTheme.colorScheme.background),
|
||||
shape = RectangleShape,
|
||||
) {
|
||||
var isRestoringSession by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
// TODO PIN-related
|
||||
// if (appPreferences.signInAutomatically) {
|
||||
try {
|
||||
serverRepository.restoreSession(
|
||||
appPreferences.currentServerId?.toUUIDOrNull(),
|
||||
appPreferences.currentUserId?.toUUIDOrNull(),
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception restoring session")
|
||||
}
|
||||
// }
|
||||
isRestoringSession = false
|
||||
}
|
||||
val current by serverRepository.current.observeAsState()
|
||||
// val backStack = rememberNavBackStack(SetupDestination.Loading)
|
||||
// setupNavigationManager.backStack = backStack
|
||||
val backStack = setupNavigationManager.backStack
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
entryDecorators =
|
||||
listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator(),
|
||||
),
|
||||
entryProvider = { key ->
|
||||
key as SetupDestination
|
||||
NavEntry(key) {
|
||||
when (key) {
|
||||
SetupDestination.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.border,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val preferences =
|
||||
UserPreferences(
|
||||
appPreferences,
|
||||
current?.userDto?.configuration ?: DefaultUserConfiguration,
|
||||
)
|
||||
SetupDestination.ServerList -> {
|
||||
SwitchServerContent(Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
if (isRestoringSession) {
|
||||
Box(
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.border,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// TODO PIN-related
|
||||
// DisposableEffect(Unit) {
|
||||
// onDispose {
|
||||
// if (!appPreferences.signInAutomatically || current?.user?.hasPin == true) {
|
||||
// serverRepository.closeSession()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
key(current?.server?.id, current?.user?.id) {
|
||||
// TODO PIN-related
|
||||
// LaunchedEffect(current?.user?.pin) {
|
||||
// if (current?.user?.pin?.isNotNullOrBlank() == true) {
|
||||
// // If user has a pin, then obscure the window in previews
|
||||
// window?.setFlags(
|
||||
// WindowManager.LayoutParams.FLAG_SECURE,
|
||||
// WindowManager.LayoutParams.FLAG_SECURE,
|
||||
// )
|
||||
// } else {
|
||||
// window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
// }
|
||||
// }
|
||||
val initialDestination =
|
||||
when {
|
||||
current != null -> Destination.Home()
|
||||
is SetupDestination.UserList -> {
|
||||
SwitchUserContent(
|
||||
currentServer = key.server,
|
||||
Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO PIN-related
|
||||
// !appPreferences.signInAutomatically -> Destination.ServerList
|
||||
|
||||
else -> Destination.ServerList
|
||||
}
|
||||
val backStack = rememberNavBackStack(initialDestination)
|
||||
navigationManager.backStack = backStack
|
||||
if (UpdateChecker.ACTIVE && appPreferences.autoCheckForUpdates) {
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
updateChecker.maybeShowUpdateToast(appPreferences.updateUrl)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Failed to check for update")
|
||||
is SetupDestination.AppContent -> {
|
||||
val current = key.current
|
||||
ProvideLocalClock {
|
||||
if (UpdateChecker.ACTIVE && appPreferences.autoCheckForUpdates) {
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
updateChecker.maybeShowUpdateToast(
|
||||
appPreferences.updateUrl,
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(
|
||||
ex,
|
||||
"Exception during update check",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val appPreferences by userPreferencesDataStore.data.collectAsState(
|
||||
appPreferences,
|
||||
)
|
||||
val preferences =
|
||||
remember(appPreferences, current) {
|
||||
UserPreferences(
|
||||
appPreferences,
|
||||
current.userDto.configuration
|
||||
?: DefaultUserConfiguration,
|
||||
)
|
||||
}
|
||||
ApplicationContent(
|
||||
user = current.user,
|
||||
server = current.server,
|
||||
navigationManager = navigationManager,
|
||||
preferences = preferences,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(current, preferences) {
|
||||
withContext(Dispatchers.IO) {
|
||||
deviceProfileService.getOrCreateDeviceProfile(
|
||||
preferences.appPreferences.playbackPreferences,
|
||||
current?.server?.serverVersion,
|
||||
)
|
||||
}
|
||||
}
|
||||
ProvideLocalClock {
|
||||
ApplicationContent(
|
||||
user = current?.user,
|
||||
server = current?.server,
|
||||
navigationManager = navigationManager,
|
||||
preferences = preferences,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -250,12 +245,69 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onRestart() {
|
||||
super.onRestart()
|
||||
// TODO PIN-related
|
||||
Timber.i("onRestart")
|
||||
viewModel.appStart()
|
||||
// val signInAutomatically =
|
||||
// runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true
|
||||
// Timber.i("onRestart: signInAutomatically=$signInAutomatically")
|
||||
// if (!signInAutomatically || serverRepository.currentUser.value?.hasPin == true) {
|
||||
|
||||
// // TODO PIN-related
|
||||
// // if (!signInAutomatically || serverRepository.currentUser.value?.hasPin == true) {
|
||||
// if (!signInAutomatically) {
|
||||
// serverRepository.closeSession()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class MainActivityViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val preferences: DataStore<AppPreferences>,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val navigationManager: SetupNavigationManager,
|
||||
private val deviceProfileService: DeviceProfileService,
|
||||
private val backdropService: BackdropService,
|
||||
) : ViewModel() {
|
||||
fun appStart() {
|
||||
viewModelScope.launch {
|
||||
val prefs = preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||
if (prefs.signInAutomatically) {
|
||||
val current =
|
||||
withContext(Dispatchers.IO) {
|
||||
serverRepository.restoreSession(
|
||||
prefs.currentServerId?.toUUIDOrNull(),
|
||||
prefs.currentUserId?.toUUIDOrNull(),
|
||||
)
|
||||
}
|
||||
if (current != null) {
|
||||
// Restored
|
||||
navigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||
} else {
|
||||
// Did not restore
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
} else {
|
||||
navigationManager.navigateTo(SetupDestination.Loading)
|
||||
backdropService.clearBackdrop()
|
||||
val currentServerId = prefs.currentServerId?.toUUIDOrNull()
|
||||
if (currentServerId != null) {
|
||||
val currentServer =
|
||||
withContext(Dispatchers.IO) {
|
||||
serverRepository.serverDao.getServer(currentServerId)?.server
|
||||
}
|
||||
if (currentServer != null) {
|
||||
navigationManager.navigateTo(SetupDestination.UserList(currentServer))
|
||||
} else {
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
} else {
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
// Create the mediaCodecCapabilitiesTest if needed
|
||||
deviceProfileService.mediaCodecCapabilitiesTest.supportsAVC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.util.EqualityMutableLiveData
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||
|
|
@ -68,44 +69,46 @@ class ServerRepository
|
|||
suspend fun changeUser(
|
||||
server: JellyfinServer,
|
||||
user: JellyfinUser,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
if (server.id != user.serverId) {
|
||||
throw IllegalStateException("User is not part of the server")
|
||||
}
|
||||
Timber.v("Changing user to ${user.name} on ${server.url}")
|
||||
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
||||
val userDto by apiClient.userApi.getCurrentUser()
|
||||
val updatedServer =
|
||||
try {
|
||||
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
|
||||
server.copy(name = sysInfo.serverName, version = sysInfo.version)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Exception fetching public system info")
|
||||
server
|
||||
): CurrentUser? =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (server.id != user.serverId) {
|
||||
throw IllegalStateException("User is not part of the server")
|
||||
}
|
||||
var updatedUser =
|
||||
user.copy(
|
||||
id = userDto.id,
|
||||
name = userDto.name,
|
||||
)
|
||||
serverDao.addOrUpdateServer(updatedServer)
|
||||
updatedUser = serverDao.addOrUpdateUser(updatedUser)
|
||||
userPreferencesDataStore.updateData {
|
||||
it
|
||||
.toBuilder()
|
||||
.apply {
|
||||
currentServerId = updatedServer.id.toServerString()
|
||||
currentUserId = updatedUser.id.toServerString()
|
||||
}.build()
|
||||
Timber.v("Changing user to ${user.name} on ${server.url}")
|
||||
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
||||
val userDto by apiClient.userApi.getCurrentUser()
|
||||
val updatedServer =
|
||||
try {
|
||||
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
|
||||
server.copy(name = sysInfo.serverName, version = sysInfo.version)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Exception fetching public system info")
|
||||
server
|
||||
}
|
||||
var updatedUser =
|
||||
user.copy(
|
||||
id = userDto.id,
|
||||
name = userDto.name,
|
||||
)
|
||||
serverDao.addOrUpdateServer(updatedServer)
|
||||
updatedUser = serverDao.addOrUpdateUser(updatedUser)
|
||||
userPreferencesDataStore.updateData {
|
||||
it
|
||||
.toBuilder()
|
||||
.apply {
|
||||
currentServerId = updatedServer.id.toServerString()
|
||||
currentUserId = updatedUser.id.toServerString()
|
||||
}.build()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_current.value = CurrentUser(updatedServer, updatedUser, userDto)
|
||||
}
|
||||
sharedPreferences.edit(true) {
|
||||
putString(SERVER_URL_KEY, updatedServer.url)
|
||||
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
|
||||
}
|
||||
return@withContext _current.value
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_current.value = CurrentUser(updatedServer, updatedUser, userDto)
|
||||
}
|
||||
sharedPreferences.edit(true) {
|
||||
putString(SERVER_URL_KEY, updatedServer.url)
|
||||
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a session for the given server & user such as when the app reopens
|
||||
|
|
@ -115,10 +118,10 @@ class ServerRepository
|
|||
suspend fun restoreSession(
|
||||
serverId: UUID?,
|
||||
userId: UUID?,
|
||||
): Boolean {
|
||||
): CurrentUser? {
|
||||
if (serverId == null || userId == null) {
|
||||
_current.setValueOnMain(null)
|
||||
return false
|
||||
return null
|
||||
}
|
||||
val serverAndUsers =
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
@ -129,13 +132,17 @@ class ServerRepository
|
|||
if (user != null) {
|
||||
// TODO pin-related
|
||||
// if (user != null && !user.hasPin) {
|
||||
changeUser(serverAndUsers.server, user)
|
||||
return true
|
||||
return changeUser(serverAndUsers.server, user)
|
||||
}
|
||||
}
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun fetchLastUsedServer(serverId: UUID?): JellyfinServer? =
|
||||
withContext(Dispatchers.IO) {
|
||||
serverId?.let { serverDao.getServer(serverId)?.server }
|
||||
}
|
||||
|
||||
fun closeSession() {
|
||||
_current.value = null
|
||||
}
|
||||
|
|
@ -170,7 +177,7 @@ class ServerRepository
|
|||
)
|
||||
}
|
||||
if (user != null) {
|
||||
changeUser(server, user)
|
||||
return@withContext changeUser(server, user)
|
||||
} else {
|
||||
throw IllegalArgumentException("Authentication result's user was null")
|
||||
}
|
||||
|
|
@ -260,6 +267,7 @@ class ServerRepository
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CurrentUser(
|
||||
val server: JellyfinServer,
|
||||
val user: JellyfinUser,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,16 @@ import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
|||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class CollectionFolderFilter(
|
||||
val nameOverride: String? = null,
|
||||
val filter: GetItemsFilter = GetItemsFilter(),
|
||||
/**
|
||||
* Whether to use the libray's saved sort & filter
|
||||
*/
|
||||
val useSavedLibraryDisplayInfo: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetItemsFilter(
|
||||
val favorite: Boolean? = null,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ data class JellyfinServer(
|
|||
],
|
||||
indices = [Index("id", "serverId", unique = true)],
|
||||
)
|
||||
@Serializable
|
||||
data class JellyfinUser(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val rowId: Int = 0,
|
||||
|
|
|
|||
|
|
@ -851,8 +851,7 @@ val basicPreferences =
|
|||
title = R.string.ui_interface,
|
||||
preferences =
|
||||
listOf(
|
||||
// TODO PIN-related
|
||||
// AppPreference.SignInAuto,
|
||||
AppPreference.SignInAuto,
|
||||
AppPreference.HomePageItems,
|
||||
AppPreference.CombineContinueNext,
|
||||
AppPreference.RewatchNextUp,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class DeviceProfileService
|
|||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val mediaCodecCapabilitiesTest by lazy {
|
||||
val mediaCodecCapabilitiesTest by lazy {
|
||||
// Created lazily below on the IO thread since it cn take time
|
||||
MediaCodecCapabilitiesTest(context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,7 @@ class NavigationManager
|
|||
*/
|
||||
fun navigateToFromDrawer(destination: Destination) {
|
||||
goToHome()
|
||||
if (destination == Destination.ServerList || destination is Destination.UserList) {
|
||||
backStack.add(destination)
|
||||
(0..<backStack.size - 1).forEach { _ -> backStack.removeAt(0) }
|
||||
} else {
|
||||
backStack.add(destination)
|
||||
}
|
||||
backStack.add(destination)
|
||||
log()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.data.model.PlaylistInfo
|
|||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.components.baseItemKinds
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.gt
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.wholphin.ui.playback.playable
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
|
|
@ -22,6 +23,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.playlistsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CreatePlaylistDto
|
||||
|
|
@ -64,10 +66,14 @@ class PlaylistCreator
|
|||
sortBy = if (shuffled) ItemSortBy.RANDOM else null,
|
||||
limit = Playlist.MAX_SIZE,
|
||||
)
|
||||
val episodes = GetEpisodesRequestHandler.execute(api, request).content.items
|
||||
val episodes =
|
||||
GetEpisodesRequestHandler
|
||||
.execute(api, request)
|
||||
.content.items
|
||||
.convertAndAddParts(false)
|
||||
val startIndex =
|
||||
episodeId?.let { episodes.indexOfFirstOrNull { it.id == episodeId } } ?: 0
|
||||
return Playlist(episodes.map { BaseItem.from(it, api) }, startIndex)
|
||||
return Playlist(episodes, startIndex)
|
||||
}
|
||||
|
||||
suspend fun createFromPlaylistId(
|
||||
|
|
@ -82,12 +88,11 @@ class PlaylistCreator
|
|||
startIndex = startIndex,
|
||||
limit = Playlist.MAX_SIZE,
|
||||
)
|
||||
val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items
|
||||
var baseItems = items.map { BaseItem.from(it, api) }
|
||||
var items = GetPlaylistItemsRequestHandler.execute(api, request).content.items
|
||||
if (shuffled) {
|
||||
baseItems = baseItems.shuffled()
|
||||
items = items.shuffled()
|
||||
}
|
||||
return Playlist(baseItems, 0)
|
||||
return Playlist(items.convertAndAddParts(), 0)
|
||||
}
|
||||
|
||||
private suspend fun createFromCollection(
|
||||
|
|
@ -120,19 +125,20 @@ class PlaylistCreator
|
|||
),
|
||||
)
|
||||
val items =
|
||||
GetItemsRequestHandler.execute(api, request).content.items.map {
|
||||
BaseItem.from(it, api)
|
||||
}
|
||||
GetItemsRequestHandler
|
||||
.execute(api, request)
|
||||
.content.items
|
||||
.convertAndAddParts()
|
||||
return Playlist(items, 0)
|
||||
}
|
||||
|
||||
suspend fun createFrom(
|
||||
item: BaseItemDto,
|
||||
startIndex: Int = 0,
|
||||
sortAndDirection: SortAndDirection?,
|
||||
shuffled: Boolean,
|
||||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
sortAndDirection: SortAndDirection? = SortAndDirection.DEFAULT,
|
||||
shuffled: Boolean = false,
|
||||
recursive: Boolean = false,
|
||||
filter: GetItemsFilter = GetItemsFilter(),
|
||||
): PlaylistCreationResult =
|
||||
when (item.type) {
|
||||
BaseItemKind.BOX_SET,
|
||||
|
|
@ -166,7 +172,7 @@ class PlaylistCreator
|
|||
),
|
||||
)
|
||||
} else {
|
||||
PlaylistCreationResult.Error(null, "Episode has not seriesId")
|
||||
PlaylistCreationResult.Error(null, "Episode has no seriesId")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +188,7 @@ class PlaylistCreator
|
|||
),
|
||||
)
|
||||
} else {
|
||||
PlaylistCreationResult.Error(null, "Episode has not seriesId")
|
||||
PlaylistCreationResult.Error(null, "Episode has no seriesId")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -207,6 +213,26 @@ class PlaylistCreator
|
|||
)
|
||||
}
|
||||
|
||||
BaseItemKind.MOVIE,
|
||||
BaseItemKind.VIDEO,
|
||||
BaseItemKind.MUSIC_VIDEO,
|
||||
-> {
|
||||
val list =
|
||||
buildList {
|
||||
add(BaseItem(item, false))
|
||||
|
||||
if (item.partCount.gt(1)) {
|
||||
api.videosApi
|
||||
.getAdditionalPart(item.id)
|
||||
.content.items
|
||||
.map {
|
||||
BaseItem(it, false)
|
||||
}.let(::addAll)
|
||||
}
|
||||
}
|
||||
PlaylistCreationResult.Success(Playlist(list, 0))
|
||||
}
|
||||
|
||||
// Not support yet
|
||||
// BaseItemKind.AGGREGATE_FOLDER -> TODO()
|
||||
// BaseItemKind.FOLDER -> TODO()
|
||||
|
|
@ -220,6 +246,20 @@ class PlaylistCreator
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun List<BaseItemDto>.convertAndAddParts(useSeriesForPrimary: Boolean = false): List<BaseItem> =
|
||||
buildList {
|
||||
this@convertAndAddParts.forEach { ep ->
|
||||
add(BaseItem(ep, useSeriesForPrimary))
|
||||
if (ep.partCount.gt(1)) {
|
||||
val parts =
|
||||
api.videosApi.getAdditionalPart(ep.id).content.items.map { part ->
|
||||
BaseItem(part, useSeriesForPrimary)
|
||||
}
|
||||
addAll(parts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getServerPlaylists(
|
||||
mediaType: MediaType?,
|
||||
scope: CoroutineScope,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.github.damontecres.wholphin.data.CurrentUser
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.acra.ACRA
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manages navigating for setup
|
||||
*/
|
||||
@Singleton
|
||||
class SetupNavigationManager
|
||||
@Inject
|
||||
constructor() {
|
||||
var backStack: MutableList<NavKey> = mutableStateListOf(SetupDestination.Loading)
|
||||
|
||||
/**
|
||||
* Go to the specified [SetupDestination]
|
||||
*/
|
||||
fun navigateTo(destination: SetupDestination) {
|
||||
backStack[0] = destination
|
||||
log()
|
||||
}
|
||||
|
||||
private fun log() {
|
||||
val dest = backStack.lastOrNull().toString()
|
||||
Timber.i("Current setup destination: %s", dest)
|
||||
ACRA.errorReporter.putCustomData("setupDestination", dest)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed interface SetupDestination : NavKey {
|
||||
@Serializable
|
||||
data object Loading : SetupDestination
|
||||
|
||||
@Serializable
|
||||
data object ServerList : SetupDestination
|
||||
|
||||
@Serializable
|
||||
data class UserList(
|
||||
val server: JellyfinServer,
|
||||
) : SetupDestination
|
||||
|
||||
@Serializable
|
||||
data class AppContent(
|
||||
val current: CurrentUser,
|
||||
) : SetupDestination
|
||||
}
|
||||
|
|
@ -418,3 +418,10 @@ fun rememberBackDropImage(item: BaseItem): String? {
|
|||
val imageUrlService = LocalImageUrlService.current
|
||||
return remember(item) { imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this, coalescing nulls to zero, is greater than that
|
||||
*/
|
||||
fun Int?.gt(that: Int) = (this ?: 0) > that
|
||||
|
||||
fun Int?.lt(that: Int) = (this ?: 0) < that
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ fun GenreCard(
|
|||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
) {
|
||||
Timber.v("genre image=${genre?.imageUrl}")
|
||||
if (genre?.imageUrl.isNotNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model =
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.data.filter.PlayedFilter
|
|||
import com.github.damontecres.wholphin.data.filter.VideoTypeFilter
|
||||
import com.github.damontecres.wholphin.data.filter.YearFilter
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||
|
|
@ -136,12 +137,13 @@ class CollectionFolderViewModel
|
|||
val viewOptions = MutableLiveData<ViewOptions>()
|
||||
|
||||
private var useSeriesForPrimary: Boolean = true
|
||||
private lateinit var collectionFilter: CollectionFolderFilter
|
||||
|
||||
fun init(
|
||||
itemId: String,
|
||||
initialSortAndDirection: SortAndDirection?,
|
||||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
collectionFilter: CollectionFolderFilter,
|
||||
useSeriesForPrimary: Boolean,
|
||||
defaultViewOptions: ViewOptions,
|
||||
): Job =
|
||||
|
|
@ -151,6 +153,7 @@ class CollectionFolderViewModel
|
|||
context.getString(R.string.error_loading_collection, itemId),
|
||||
) + Dispatchers.IO,
|
||||
) {
|
||||
this@CollectionFolderViewModel.collectionFilter = collectionFilter
|
||||
this@CollectionFolderViewModel.useSeriesForPrimary = useSeriesForPrimary
|
||||
this@CollectionFolderViewModel.itemId = itemId
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
|
|
@ -166,15 +169,17 @@ class CollectionFolderViewModel
|
|||
)
|
||||
|
||||
val sortAndDirection =
|
||||
libraryDisplayInfo?.sortAndDirection
|
||||
?: initialSortAndDirection
|
||||
?: SortAndDirection.DEFAULT
|
||||
if (collectionFilter.useSavedLibraryDisplayInfo) {
|
||||
libraryDisplayInfo?.sortAndDirection
|
||||
} else {
|
||||
null
|
||||
} ?: initialSortAndDirection ?: SortAndDirection.DEFAULT
|
||||
|
||||
val filterToUse =
|
||||
if (libraryDisplayInfo?.filter != null) {
|
||||
filter.merge(libraryDisplayInfo.filter)
|
||||
if (collectionFilter.useSavedLibraryDisplayInfo && libraryDisplayInfo?.filter != null) {
|
||||
collectionFilter.filter.merge(libraryDisplayInfo.filter)
|
||||
} else {
|
||||
filter
|
||||
collectionFilter.filter
|
||||
}
|
||||
|
||||
loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary)
|
||||
|
|
@ -185,18 +190,20 @@ class CollectionFolderViewModel
|
|||
newSort: SortAndDirection = this.sortAndDirection.value!!,
|
||||
viewOptions: ViewOptions? = this.viewOptions.value,
|
||||
) {
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
LibraryDisplayInfo(
|
||||
userId = user.rowId,
|
||||
itemId = itemId,
|
||||
sort = newSort.sort,
|
||||
direction = newSort.direction,
|
||||
filter = newFilter,
|
||||
viewOptions = viewOptions,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
if (collectionFilter.useSavedLibraryDisplayInfo) {
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
LibraryDisplayInfo(
|
||||
userId = user.rowId,
|
||||
itemId = itemId,
|
||||
sort = newSort.sort,
|
||||
direction = newSort.direction,
|
||||
filter = newFilter,
|
||||
viewOptions = viewOptions,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -490,7 +497,7 @@ class CollectionFolderViewModel
|
|||
fun CollectionFolderGrid(
|
||||
preferences: UserPreferences,
|
||||
itemId: UUID,
|
||||
initialFilter: GetItemsFilter,
|
||||
initialFilter: CollectionFolderFilter,
|
||||
recursive: Boolean,
|
||||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
|
|
@ -523,7 +530,7 @@ fun CollectionFolderGrid(
|
|||
fun CollectionFolderGrid(
|
||||
preferences: UserPreferences,
|
||||
itemId: String,
|
||||
initialFilter: GetItemsFilter,
|
||||
initialFilter: CollectionFolderFilter,
|
||||
recursive: Boolean,
|
||||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
|
|
@ -550,7 +557,7 @@ fun CollectionFolderGrid(
|
|||
)
|
||||
}
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(SortAndDirection.DEFAULT)
|
||||
val filter by viewModel.filter.observeAsState(initialFilter)
|
||||
val filter by viewModel.filter.observeAsState(initialFilter.filter)
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading)
|
||||
val item by viewModel.item.observeAsState()
|
||||
|
|
@ -574,11 +581,17 @@ fun CollectionFolderGrid(
|
|||
|
||||
LoadingState.Success -> {
|
||||
pager?.let { pager ->
|
||||
val title =
|
||||
initialFilter.nameOverride
|
||||
?: item?.name
|
||||
?: item?.data?.collectionType?.name
|
||||
?: stringResource(R.string.collection)
|
||||
Box(modifier = modifier) {
|
||||
CollectionFolderGridContent(
|
||||
preferences,
|
||||
item,
|
||||
pager,
|
||||
preferences = preferences,
|
||||
item = item,
|
||||
title = title,
|
||||
pager = pager,
|
||||
sortAndDirection = sortAndDirection!!,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onClickItem = onClickItem,
|
||||
|
|
@ -699,6 +712,7 @@ fun CollectionFolderGrid(
|
|||
fun CollectionFolderGridContent(
|
||||
preferences: UserPreferences,
|
||||
item: BaseItem?,
|
||||
title: String,
|
||||
pager: List<BaseItem?>,
|
||||
sortAndDirection: SortAndDirection,
|
||||
onClickItem: (Int, BaseItem) -> Unit,
|
||||
|
|
@ -722,7 +736,6 @@ fun CollectionFolderGridContent(
|
|||
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection)
|
||||
|
||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||
var showViewOptions by rememberSaveable { mutableStateOf(false) }
|
||||
|
|
@ -833,8 +846,7 @@ fun CollectionFolderGridContent(
|
|||
item = focusedItem,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(.6f)
|
||||
// .fillMaxHeight(.25f)
|
||||
.fillMaxWidth()
|
||||
.height(140.dp)
|
||||
.padding(16.dp),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,14 +20,17 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.LocalTextStyle
|
||||
|
|
@ -54,22 +57,24 @@ fun DotSeparatedRow(
|
|||
text = text,
|
||||
style = textStyle,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
)
|
||||
if (communityRating != null || criticRating != null || index != texts.lastIndex) {
|
||||
Dot()
|
||||
}
|
||||
}
|
||||
val height = with(LocalDensity.current) { textStyle.fontSize.toDp() }
|
||||
communityRating?.let {
|
||||
SimpleStarRating(
|
||||
communityRating = it,
|
||||
modifier = Modifier,
|
||||
modifier = Modifier.height(height),
|
||||
)
|
||||
if (criticRating != null) {
|
||||
Dot()
|
||||
}
|
||||
}
|
||||
criticRating?.let {
|
||||
TomatoRating(it)
|
||||
TomatoRating(it, Modifier.height(height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
|
|
@ -39,6 +42,7 @@ import kotlinx.coroutines.sync.Semaphore
|
|||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
|
|
@ -55,9 +59,12 @@ class GenreViewModel
|
|||
constructor(
|
||||
private val api: ApiClient,
|
||||
private val imageUrlService: ImageUrlService,
|
||||
private val serverRepository: ServerRepository,
|
||||
val navigationManager: NavigationManager,
|
||||
) : ViewModel() {
|
||||
private lateinit var itemId: UUID
|
||||
|
||||
val item = MutableLiveData<BaseItem?>(null)
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val genres = MutableLiveData<List<Genre>>(listOf())
|
||||
|
||||
|
|
@ -65,8 +72,14 @@ class GenreViewModel
|
|||
loading.value = LoadingState.Loading
|
||||
this.itemId = itemId
|
||||
viewModelScope.launch(Dispatchers.IO + LoadingExceptionHandler(loading, "Failed to fetch genres")) {
|
||||
val item =
|
||||
api.userLibraryApi.getItem(itemId = itemId).content.let {
|
||||
BaseItem(it, false)
|
||||
}
|
||||
this@GenreViewModel.item.setValueOnMain(item)
|
||||
val request =
|
||||
GetGenresRequest(
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
parentId = itemId,
|
||||
fields = SlimItemFields,
|
||||
)
|
||||
|
|
@ -187,13 +200,23 @@ fun GenreCardGrid(
|
|||
LoadingState.Success -> {
|
||||
Box(modifier = modifier) {
|
||||
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
|
||||
val item by viewModel.item.observeAsState(null)
|
||||
CardGrid(
|
||||
pager = genres,
|
||||
onClickItem = { _, genre ->
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.FilteredCollection(
|
||||
itemId = itemId,
|
||||
filter = GetItemsFilter(genres = listOf(genre.id)),
|
||||
filter =
|
||||
CollectionFolderFilter(
|
||||
nameOverride =
|
||||
listOfNotNull(
|
||||
genre.name,
|
||||
item?.title,
|
||||
).joinToString(" "),
|
||||
filter = GetItemsFilter(genres = listOf(genre.id)),
|
||||
useSavedLibraryDisplayInfo = false,
|
||||
),
|
||||
recursive = true,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ fun SimpleStarRating(
|
|||
if (text.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = text,
|
||||
maxLines = 1,
|
||||
modifier = Modifier,
|
||||
)
|
||||
val height = with(LocalDensity.current) { LocalTextStyle.current.fontSize.toDp() }
|
||||
|
|
@ -130,6 +131,7 @@ fun TomatoRating(
|
|||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
Text(
|
||||
text = rating.toInt().toString() + "%",
|
||||
maxLines = 1,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,15 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
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.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.main.buildCombinedNextUp
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -57,6 +58,7 @@ class RecommendedTvShowViewModel
|
|||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val preferencesDataStore: DataStore<AppPreferences>,
|
||||
private val datePlayedService: DatePlayedService,
|
||||
@Assisted val parentId: UUID,
|
||||
navigationManager: NavigationManager,
|
||||
favoriteWatchManager: FavoriteWatchManager,
|
||||
|
|
@ -78,17 +80,17 @@ class RecommendedTvShowViewModel
|
|||
|
||||
override fun init() {
|
||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||
val itemsPerRow =
|
||||
preferencesDataStore.data
|
||||
.firstOrNull()
|
||||
?.homePagePreferences
|
||||
?.maxItemsPerRow
|
||||
?: AppPreference.HomePageItems.defaultValue.toInt()
|
||||
val preferences =
|
||||
preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||
val combineNextUp = preferences.homePagePreferences.combineContinueNext
|
||||
val itemsPerRow = preferences.homePagePreferences.maxItemsPerRow
|
||||
val userId = serverRepository.currentUser.value?.id
|
||||
try {
|
||||
val resumeItemsDeferred =
|
||||
viewModelScope.async(Dispatchers.IO) {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
userId = userId,
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
|
|
@ -106,12 +108,14 @@ class RecommendedTvShowViewModel
|
|||
viewModelScope.async(Dispatchers.IO) {
|
||||
val nextUpRequest =
|
||||
GetNextUpRequest(
|
||||
parentId = parentId,
|
||||
userId = userId,
|
||||
fields = SlimItemFields,
|
||||
enableUserData = true,
|
||||
startIndex = 0,
|
||||
imageTypeLimit = 1,
|
||||
parentId = parentId,
|
||||
limit = itemsPerRow,
|
||||
enableTotalRecordCount = false,
|
||||
enableResumable = false,
|
||||
enableUserData = true,
|
||||
enableRewatching = preferences.homePagePreferences.enableRewatchingNextUp,
|
||||
)
|
||||
|
||||
GetNextUpRequestHandler
|
||||
|
|
@ -119,21 +123,45 @@ class RecommendedTvShowViewModel
|
|||
.toBaseItems(api, true)
|
||||
}
|
||||
val resumeItems = resumeItemsDeferred.await()
|
||||
update(
|
||||
R.string.continue_watching,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.continue_watching),
|
||||
resumeItems,
|
||||
),
|
||||
)
|
||||
val nextUpItems = nextUpItemsDeferred.await()
|
||||
update(
|
||||
R.string.next_up,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.next_up),
|
||||
nextUpItems,
|
||||
),
|
||||
)
|
||||
if (combineNextUp) {
|
||||
val combined =
|
||||
buildCombinedNextUp(
|
||||
viewModelScope,
|
||||
datePlayedService,
|
||||
resumeItems,
|
||||
nextUpItems,
|
||||
)
|
||||
update(
|
||||
R.string.continue_watching,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.continue_watching),
|
||||
combined,
|
||||
),
|
||||
)
|
||||
update(
|
||||
R.string.next_up,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.next_up),
|
||||
listOf(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
update(
|
||||
R.string.continue_watching,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.continue_watching),
|
||||
resumeItems,
|
||||
),
|
||||
)
|
||||
update(
|
||||
R.string.next_up,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.next_up),
|
||||
nextUpItems,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (resumeItems.isNotEmpty() || nextUpItems.isNotEmpty()) {
|
||||
loading.setValueOnMain(LoadingState.Success)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
|
|
@ -28,7 +28,7 @@ fun CollectionFolderBoxSet(
|
|||
item: BaseItem?,
|
||||
recursive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
filter: GetItemsFilter = GetItemsFilter(),
|
||||
filter: CollectionFolderFilter = CollectionFolderFilter(),
|
||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||
playEnabled: Boolean = false,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
|
|
@ -29,7 +29,7 @@ fun CollectionFolderGeneric(
|
|||
recursive: Boolean,
|
||||
playEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
filter: GetItemsFilter = GetItemsFilter(),
|
||||
filter: CollectionFolderFilter = CollectionFolderFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
sortOptions: List<ItemSortBy> = VideoSortOptions,
|
||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
|
|
@ -164,7 +164,7 @@ fun CollectionFolderLiveTv(
|
|||
preferences = preferences,
|
||||
onClickItem = onClickItem,
|
||||
itemId = folders[folderIndex].id,
|
||||
initialFilter = GetItemsFilter(),
|
||||
initialFilter = CollectionFolderFilter(),
|
||||
showTitle = false,
|
||||
recursive = false,
|
||||
sortOptions = VideoSortOptions,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
|
|
@ -115,8 +116,11 @@ fun CollectionFolderMovie(
|
|||
},
|
||||
itemId = destination.itemId,
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
@ -143,8 +147,11 @@ fun CollectionFolderMovie(
|
|||
},
|
||||
itemId = destination.itemId,
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.BOX_SET),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.BOX_SET),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
|
||||
|
|
@ -25,7 +25,7 @@ fun CollectionFolderPlaylist(
|
|||
item: BaseItem?,
|
||||
recursive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
filter: GetItemsFilter = GetItemsFilter(),
|
||||
filter: CollectionFolderFilter = CollectionFolderFilter(),
|
||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||
) {
|
||||
var showHeader by remember { mutableStateOf(true) }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
|
||||
|
|
@ -23,7 +23,7 @@ fun CollectionFolderRecordings(
|
|||
itemId: UUID,
|
||||
recursive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
filter: GetItemsFilter = GetItemsFilter(),
|
||||
filter: CollectionFolderFilter = CollectionFolderFilter(),
|
||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||
) {
|
||||
var showHeader by remember { mutableStateOf(true) }
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
|
|
@ -115,8 +116,11 @@ fun CollectionFolderTv(
|
|||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultForFavoritesFilterOptions
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
|
|
@ -117,9 +118,12 @@ fun FavoritesPage(
|
|||
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||
itemId = "${NavDrawerItem.Favorites.id}_movies",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
@ -145,9 +149,12 @@ fun FavoritesPage(
|
|||
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||
itemId = "${NavDrawerItem.Favorites.id}_series",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
@ -173,9 +180,12 @@ fun FavoritesPage(
|
|||
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||
itemId = "${NavDrawerItem.Favorites.id}_episodes",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
@ -202,9 +212,12 @@ fun FavoritesPage(
|
|||
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||
itemId = "${NavDrawerItem.Favorites.id}_videos",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.VIDEO),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.VIDEO),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
@ -230,9 +243,12 @@ fun FavoritesPage(
|
|||
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||
itemId = "${NavDrawerItem.Favorites.id}_playlists",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.PLAYLIST),
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
includeItemTypes = listOf(BaseItemKind.PLAYLIST),
|
||||
),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
|
|
@ -258,9 +274,12 @@ fun FavoritesPage(
|
|||
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||
itemId = "${NavDrawerItem.Favorites.id}_people",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
override = GetItemsFilterOverride.PERSON,
|
||||
CollectionFolderFilter(
|
||||
filter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
override = GetItemsFilterOverride.PERSON,
|
||||
),
|
||||
),
|
||||
initialSortAndDirection =
|
||||
SortAndDirection(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -59,6 +60,7 @@ import com.github.damontecres.wholphin.ui.components.TabRow
|
|||
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.logTab
|
||||
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.ui.util.rememberDelayedNestedScroll
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -249,6 +251,12 @@ fun SeriesOverviewContent(
|
|||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}
|
||||
}.onKeyEvent {
|
||||
if (episode != null && isPlayKeyUp(it)) {
|
||||
onClick.invoke(episode)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
return@onKeyEvent false
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
cardHeight = 120.dp,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.github.damontecres.wholphin.util.LoadingState
|
|||
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -90,6 +91,7 @@ class HomeViewModel
|
|||
),
|
||||
) {
|
||||
Timber.d("init HomeViewModel")
|
||||
backdropService.clearBackdrop()
|
||||
|
||||
serverRepository.currentUserDto.value?.let { userDto ->
|
||||
val includedIds =
|
||||
|
|
@ -109,7 +111,13 @@ class HomeViewModel
|
|||
val watching =
|
||||
buildList {
|
||||
if (prefs.combineContinueNext) {
|
||||
val items = buildCombined(resume, nextUp)
|
||||
val items =
|
||||
buildCombinedNextUp(
|
||||
viewModelScope,
|
||||
datePlayedService,
|
||||
resume,
|
||||
nextUp,
|
||||
)
|
||||
add(
|
||||
HomeRowLoadingState.Success(
|
||||
title = context.getString(R.string.continue_watching),
|
||||
|
|
@ -267,39 +275,6 @@ class HomeViewModel
|
|||
latestRows.setValueOnMain(rows)
|
||||
}
|
||||
|
||||
private suspend fun buildCombined(
|
||||
resume: List<BaseItem>,
|
||||
nextUp: List<BaseItem>,
|
||||
): List<BaseItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val start = System.currentTimeMillis()
|
||||
val semaphore = Semaphore(3)
|
||||
val deferred =
|
||||
nextUp
|
||||
.filter { it.data.seriesId != null }
|
||||
.map { item ->
|
||||
viewModelScope.async(Dispatchers.IO) {
|
||||
try {
|
||||
semaphore.withPermit {
|
||||
datePlayedService.getLastPlayed(item)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", item.id)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nextUpLastPlayed = deferred.awaitAll()
|
||||
val timestamps = mutableMapOf<UUID, LocalDateTime?>()
|
||||
nextUp.map { it.id }.zip(nextUpLastPlayed).toMap(timestamps)
|
||||
resume.forEach { timestamps[it.id] = it.data.userData?.lastPlayedDate }
|
||||
val result = (resume + nextUp).sortedByDescending { timestamps[it.id] }
|
||||
val duration = (System.currentTimeMillis() - start).milliseconds
|
||||
Timber.v("buildCombined took %s", duration)
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
fun setWatched(
|
||||
itemId: UUID,
|
||||
played: Boolean,
|
||||
|
|
@ -340,3 +315,38 @@ data class LatestData(
|
|||
val title: String,
|
||||
val request: GetLatestMediaRequest,
|
||||
)
|
||||
|
||||
suspend fun buildCombinedNextUp(
|
||||
scope: CoroutineScope,
|
||||
datePlayedService: DatePlayedService,
|
||||
resume: List<BaseItem>,
|
||||
nextUp: List<BaseItem>,
|
||||
): List<BaseItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val start = System.currentTimeMillis()
|
||||
val semaphore = Semaphore(3)
|
||||
val deferred =
|
||||
nextUp
|
||||
.filter { it.data.seriesId != null }
|
||||
.map { item ->
|
||||
scope.async(Dispatchers.IO) {
|
||||
try {
|
||||
semaphore.withPermit {
|
||||
datePlayedService.getLastPlayed(item)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", item.id)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nextUpLastPlayed = deferred.awaitAll()
|
||||
val timestamps = mutableMapOf<UUID, LocalDateTime?>()
|
||||
nextUp.map { it.id }.zip(nextUpLastPlayed).toMap(timestamps)
|
||||
resume.forEach { timestamps[it.id] = it.data.userData?.lastPlayedDate }
|
||||
val result = (resume + nextUp).sortedByDescending { timestamps[it.id] }
|
||||
val duration = (System.currentTimeMillis() - start).milliseconds
|
||||
Timber.v("buildCombined took %s", duration)
|
||||
return@withContext result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.saveable.rememberSerializable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
|
@ -24,8 +25,12 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.runtime.serialization.NavBackStackSerializer
|
||||
import androidx.navigation3.runtime.serialization.NavKeySerializer
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import coil3.compose.AsyncImage
|
||||
|
|
@ -62,13 +67,22 @@ class ApplicationContentViewModel
|
|||
*/
|
||||
@Composable
|
||||
fun ApplicationContent(
|
||||
server: JellyfinServer?,
|
||||
user: JellyfinUser?,
|
||||
server: JellyfinServer,
|
||||
user: JellyfinUser,
|
||||
navigationManager: NavigationManager,
|
||||
preferences: UserPreferences,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ApplicationContentViewModel = hiltViewModel(),
|
||||
) {
|
||||
val backStack: MutableList<NavKey> =
|
||||
rememberSerializable(
|
||||
server,
|
||||
user,
|
||||
serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()),
|
||||
) {
|
||||
NavBackStack(Destination.Home())
|
||||
}
|
||||
navigationManager.backStack = backStack
|
||||
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
|
||||
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ package com.github.damontecres.wholphin.ui.nav
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||
|
|
@ -29,14 +29,6 @@ import java.util.UUID
|
|||
sealed class Destination(
|
||||
val fullScreen: Boolean = false,
|
||||
) : NavKey {
|
||||
@Serializable
|
||||
data object ServerList : Destination(true)
|
||||
|
||||
@Serializable
|
||||
data class UserList(
|
||||
val server: JellyfinServer,
|
||||
) : Destination(true)
|
||||
|
||||
@Serializable
|
||||
data class Home(
|
||||
val id: Long = 0L,
|
||||
|
|
@ -104,7 +96,7 @@ sealed class Destination(
|
|||
@Serializable
|
||||
data class FilteredCollection(
|
||||
val itemId: UUID,
|
||||
val filter: GetItemsFilter,
|
||||
val filter: CollectionFolderFilter,
|
||||
val recursive: Boolean,
|
||||
) : Destination(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ import com.github.damontecres.wholphin.ui.playback.PlaybackPage
|
|||
import com.github.damontecres.wholphin.ui.preferences.PreferencesPage
|
||||
import com.github.damontecres.wholphin.ui.seerr.SeerrDiscoverPage
|
||||
import com.github.damontecres.wholphin.ui.setup.InstallUpdatePage
|
||||
import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
|
||||
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import timber.log.Timber
|
||||
|
|
@ -69,14 +67,6 @@ fun DestinationContent(
|
|||
)
|
||||
}
|
||||
|
||||
Destination.ServerList -> {
|
||||
SwitchServerContent(modifier)
|
||||
}
|
||||
|
||||
is Destination.UserList -> {
|
||||
SwitchUserContent(destination.server, modifier)
|
||||
}
|
||||
|
||||
is Destination.Settings -> {
|
||||
PreferencesPage(
|
||||
preferences.appPreferences,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ import com.github.damontecres.wholphin.preferences.AppThemeColors
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
|
|
@ -106,6 +108,7 @@ class NavDrawerViewModel
|
|||
constructor(
|
||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
val navigationManager: NavigationManager,
|
||||
val setupNavigationManager: SetupNavigationManager,
|
||||
val backdropService: BackdropService,
|
||||
) : ViewModel() {
|
||||
private var all: List<NavDrawerItem>? = null
|
||||
|
|
@ -375,10 +378,8 @@ fun NavDrawer(
|
|||
drawerOpen = drawerState.isOpen,
|
||||
interactionSource = interactionSource,
|
||||
onClick = {
|
||||
viewModel.navigationManager.navigateToFromDrawer(
|
||||
Destination.UserList(
|
||||
server,
|
||||
),
|
||||
viewModel.setupNavigationManager.navigateTo(
|
||||
SetupDestination.UserList(server),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(start = drawerPadding),
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ fun DownloadSubtitlesContent(
|
|||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
// val lang = rememberTextFieldState(language)
|
||||
var lang by rememberSaveable { mutableStateOf("") }
|
||||
var lang by rememberSaveable { mutableStateOf(language) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
|
|||
|
|
@ -286,11 +286,12 @@ class PlaybackViewModel
|
|||
playNextUp()
|
||||
}
|
||||
|
||||
if (!isPlaylist && queriedItem.type == BaseItemKind.EPISODE) {
|
||||
val playlist =
|
||||
playlistCreator.createFromEpisode(queriedItem.seriesId!!, queriedItem.id)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.playlist.value = playlist
|
||||
if (!isPlaylist) {
|
||||
val result = playlistCreator.createFrom(queriedItem)
|
||||
if (result is PlaylistCreationResult.Success && result.playlist.items.isNotEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.playlist.value = result.playlist
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import com.github.damontecres.wholphin.R
|
|||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
|
|
@ -41,7 +41,7 @@ class SwitchServerViewModel
|
|||
val jellyfin: Jellyfin,
|
||||
val serverRepository: ServerRepository,
|
||||
val serverDao: JellyfinServerDao,
|
||||
val navigationManager: NavigationManager,
|
||||
val navigationManager: SetupNavigationManager,
|
||||
) : ViewModel() {
|
||||
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
|
||||
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
|
||||
|
|
@ -156,7 +156,7 @@ class SwitchServerViewModel
|
|||
)
|
||||
serverRepository.addAndChangeServer(updatedServer)
|
||||
withContext(Dispatchers.Main) {
|
||||
navigationManager.navigateTo(Destination.UserList(server))
|
||||
navigationManager.navigateTo(SetupDestination.UserList(updatedServer))
|
||||
}
|
||||
} else if (result is ServerConnectionStatus.Error) {
|
||||
showToast(context, "Error connecting: $${result.message}")
|
||||
|
|
@ -201,7 +201,7 @@ class SwitchServerViewModel
|
|||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
addServerState.value = LoadingState.Success
|
||||
navigationManager.navigateTo(Destination.UserList(server))
|
||||
navigationManager.navigateTo(SetupDestination.UserList(server))
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.tv.material3.Text
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||
|
|
@ -130,7 +131,9 @@ fun SwitchUserContent(
|
|||
viewModel.removeUser(user)
|
||||
},
|
||||
onSwitchServer = {
|
||||
viewModel.navigationManager.navigateTo(Destination.ServerList)
|
||||
viewModel.setupNavigationManager.navigateTo(
|
||||
SetupDestination.ServerList,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer
|
|||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -43,6 +45,7 @@ class SwitchUserViewModel
|
|||
val serverRepository: ServerRepository,
|
||||
val serverDao: JellyfinServerDao,
|
||||
val navigationManager: NavigationManager,
|
||||
val setupNavigationManager: SetupNavigationManager,
|
||||
val imageUrlService: ImageUrlService,
|
||||
@Assisted val server: JellyfinServer,
|
||||
) : ViewModel() {
|
||||
|
|
@ -120,9 +123,11 @@ class SwitchUserViewModel
|
|||
fun switchUser(user: JellyfinUser) {
|
||||
viewModelScope.launchIO {
|
||||
try {
|
||||
serverRepository.changeUser(server, user)
|
||||
withContext(Dispatchers.Main) {
|
||||
navigationManager.goToHome()
|
||||
val current = serverRepository.changeUser(server, user)
|
||||
if (current != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error switching user")
|
||||
|
|
@ -144,9 +149,11 @@ class SwitchUserViewModel
|
|||
username = username,
|
||||
password = password,
|
||||
)
|
||||
serverRepository.changeUser(server.url, authenticationResult)
|
||||
withContext(Dispatchers.Main) {
|
||||
navigationManager.goToHome()
|
||||
val current = serverRepository.changeUser(server.url, authenticationResult)
|
||||
if (current != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error logging in user")
|
||||
|
|
@ -192,9 +199,13 @@ class SwitchUserViewModel
|
|||
val authenticationResult by api.userApi.authenticateWithQuickConnect(
|
||||
QuickConnectDto(secret = state.secret),
|
||||
)
|
||||
serverRepository.changeUser(server.url, authenticationResult)
|
||||
withContext(Dispatchers.Main) {
|
||||
navigationManager.goToHome()
|
||||
val current = serverRepository.changeUser(server.url, authenticationResult)
|
||||
if (current != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
setupNavigationManager.navigateTo(
|
||||
SetupDestination.AppContent(current),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error during quick connect")
|
||||
|
|
|
|||
|
|
@ -38,9 +38,17 @@ object MPVLib {
|
|||
|
||||
external fun create(appctx: Context)
|
||||
|
||||
external fun init()
|
||||
fun initialize() {
|
||||
synchronized(this) { init() }
|
||||
}
|
||||
|
||||
external fun destroy()
|
||||
private external fun init()
|
||||
|
||||
fun tearDown() {
|
||||
synchronized(this) { destroy() }
|
||||
}
|
||||
|
||||
private external fun destroy()
|
||||
|
||||
external fun attachSurface(surface: Surface)
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class MpvPlayer(
|
|||
|
||||
private var surface: Surface? = null
|
||||
private val playbackState = AtomicReference<PlaybackState>(PlaybackState.EMPTY)
|
||||
private val mpvLogger = MpvLogger()
|
||||
|
||||
// This looper will sent events to the main thread
|
||||
private val looper = Util.getCurrentOrMainLooper()
|
||||
|
|
@ -87,7 +88,7 @@ class MpvPlayer(
|
|||
) { listener, eventFlags ->
|
||||
listener.onEvents(this@MpvPlayer, Player.Events(eventFlags))
|
||||
}
|
||||
private val availableCommands: Player.Commands
|
||||
private var availableCommands: Player.Commands = Player.Commands.Builder().build()
|
||||
private val trackSelector = DefaultTrackSelector(context)
|
||||
|
||||
// This thread/looper will receive commands from the main thread to execute
|
||||
|
|
@ -101,7 +102,13 @@ class MpvPlayer(
|
|||
private set
|
||||
|
||||
init {
|
||||
sendCommand(MpvCommand.INITIALIZE, null)
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
Timber.v("config-dir=${context.filesDir.path}")
|
||||
MPVLib.addLogObserver(mpvLogger)
|
||||
|
||||
MPVLib.create(context)
|
||||
MPVLib.setOptionString("config", "yes")
|
||||
MPVLib.setOptionString("config-dir", context.filesDir.path)
|
||||
|
|
@ -120,17 +127,14 @@ class MpvPlayer(
|
|||
MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
|
||||
MPVLib.init()
|
||||
MPVLib.initialize()
|
||||
|
||||
MPVLib.setOptionString("force-window", "no")
|
||||
MPVLib.setOptionString("idle", "yes")
|
||||
// MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath)
|
||||
|
||||
internalHandler.post {
|
||||
MPVLib.addObserver(this@MpvPlayer)
|
||||
MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
|
||||
MPVLib.addLogObserver(MpvLogger())
|
||||
}
|
||||
MPVLib.addObserver(this@MpvPlayer)
|
||||
MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
|
||||
|
||||
availableCommands =
|
||||
Player.Commands
|
||||
|
|
@ -298,15 +302,14 @@ class MpvPlayer(
|
|||
|
||||
override fun release() {
|
||||
Timber.i("release")
|
||||
internalHandler.removeCallbacks(updatePlaybackState)
|
||||
playbackState.update {
|
||||
PlaybackState.EMPTY
|
||||
}
|
||||
if (!isReleased) {
|
||||
thread.quit()
|
||||
MPVLib.removeObserver(this)
|
||||
clearVideoSurfaceView(null)
|
||||
MPVLib.destroy()
|
||||
internalHandler.removeCallbacks(updatePlaybackState)
|
||||
MPVLib.removeObserver(this@MpvPlayer)
|
||||
sendCommand(MpvCommand.DESTROY, null)
|
||||
thread.quitSafely()
|
||||
}
|
||||
isReleased = true
|
||||
}
|
||||
|
|
@ -466,15 +469,9 @@ class MpvPlayer(
|
|||
throwIfReleased()
|
||||
if (DEBUG) Timber.v("setVideoSurfaceView")
|
||||
val surface = surfaceView?.holder?.surface
|
||||
if (surface != null) {
|
||||
this.surface = surface
|
||||
if (surface != null && surface.isValid) {
|
||||
Timber.v("Queued attach")
|
||||
MPVLib.attachSurface(surface)
|
||||
MPVLib.setOptionString("force-window", "yes")
|
||||
Timber.d("Attached surface")
|
||||
playbackState.load().media?.let {
|
||||
sendCommand(MpvCommand.LOAD_FILE, it)
|
||||
}
|
||||
sendCommand(MpvCommand.ATTACH_SURFACE, surface)
|
||||
} else {
|
||||
clearVideoSurfaceView(null)
|
||||
}
|
||||
|
|
@ -483,9 +480,7 @@ class MpvPlayer(
|
|||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
if (surface == surfaceView?.holder?.surface) {
|
||||
Timber.d("clearVideoSurfaceView")
|
||||
MPVLib.detachSurface()
|
||||
MPVLib.setPropertyString("vo", "null")
|
||||
MPVLib.setPropertyString("force-window", "no")
|
||||
sendCommand(MpvCommand.ATTACH_SURFACE, null)
|
||||
} else {
|
||||
Timber.w("clearVideoSurfaceView called with different surface")
|
||||
}
|
||||
|
|
@ -621,7 +616,7 @@ class MpvPlayer(
|
|||
}
|
||||
|
||||
override fun event(eventId: Int) {
|
||||
Timber.v("event: thread=${Thread.currentThread().name}, eventId=$eventId")
|
||||
Timber.v("event: eventId=%s", eventId)
|
||||
when (eventId) {
|
||||
MPV_EVENT_START_FILE -> {
|
||||
internalHandler.post(updatePlaybackState)
|
||||
|
|
@ -891,6 +886,33 @@ class MpvPlayer(
|
|||
MpvCommand.LOAD_FILE -> {
|
||||
loadFile(msg.obj as MediaAndPosition)
|
||||
}
|
||||
|
||||
MpvCommand.ATTACH_SURFACE -> {
|
||||
val surface = msg.obj as Surface?
|
||||
if (surface == null || (this.surface != null && this.surface != surface)) {
|
||||
// If clearing or changing the surface
|
||||
MPVLib.detachSurface()
|
||||
MPVLib.setPropertyString("vo", "null")
|
||||
MPVLib.setPropertyString("force-window", "no")
|
||||
}
|
||||
if (surface != null) {
|
||||
MPVLib.attachSurface(surface)
|
||||
this.surface = surface
|
||||
MPVLib.setOptionString("force-window", "yes")
|
||||
Timber.d("Attached surface")
|
||||
}
|
||||
}
|
||||
|
||||
MpvCommand.INITIALIZE -> {
|
||||
init()
|
||||
}
|
||||
|
||||
MpvCommand.DESTROY -> {
|
||||
clearVideoSurfaceView(null)
|
||||
MPVLib.removeLogObserver(mpvLogger)
|
||||
MPVLib.tearDown()
|
||||
Timber.d("MPVLib destroyed")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -1020,4 +1042,7 @@ enum class MpvCommand {
|
|||
SET_SPEED,
|
||||
SET_SUBTITLE_DELAY,
|
||||
LOAD_FILE,
|
||||
ATTACH_SURFACE,
|
||||
INITIALIZE,
|
||||
DESTROY,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue