mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
Merge branch 'main' into develop/mpv
This commit is contained in:
commit
c8b6394a5d
55 changed files with 1267 additions and 575 deletions
|
|
@ -12,6 +12,7 @@ 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
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
|
@ -99,7 +100,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
) {
|
) {
|
||||||
var isRestoringSession by remember { mutableStateOf(true) }
|
var isRestoringSession by remember { mutableStateOf(true) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (appPreferences.currentServerId.isNotBlank() && appPreferences.currentUserId.isNotBlank()) {
|
|
||||||
try {
|
try {
|
||||||
serverRepository.restoreSession(
|
serverRepository.restoreSession(
|
||||||
appPreferences.currentServerId?.toUUIDOrNull(),
|
appPreferences.currentServerId?.toUUIDOrNull(),
|
||||||
|
|
@ -108,13 +108,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Exception restoring session")
|
Timber.e(ex, "Exception restoring session")
|
||||||
}
|
}
|
||||||
Timber.d("MainActivity session restored")
|
|
||||||
}
|
|
||||||
isRestoringSession = false
|
isRestoringSession = false
|
||||||
}
|
}
|
||||||
val server = serverRepository.currentServer
|
val server by serverRepository.currentServer.observeAsState()
|
||||||
val user = serverRepository.currentUser
|
val user by serverRepository.currentUser.observeAsState()
|
||||||
val userDto = serverRepository.currentUserDto
|
val userDto by serverRepository.currentUserDto.observeAsState()
|
||||||
|
|
||||||
val preferences =
|
val preferences =
|
||||||
UserPreferences(
|
UserPreferences(
|
||||||
|
|
@ -142,8 +140,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
val initialDestination =
|
val initialDestination =
|
||||||
if (server != null && user != null) {
|
if (server != null && user != null) {
|
||||||
Destination.Home()
|
Destination.Home()
|
||||||
} else if (server != null) {
|
|
||||||
Destination.UserList
|
|
||||||
} else {
|
} else {
|
||||||
Destination.ServerList
|
Destination.ServerList
|
||||||
}
|
}
|
||||||
|
|
@ -158,9 +154,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(server, user) {
|
|
||||||
serverEventListener.init(server, user)
|
|
||||||
}
|
|
||||||
ApplicationContent(
|
ApplicationContent(
|
||||||
user = user,
|
user = user,
|
||||||
server = server,
|
server = server,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class ItemPlaybackRepository
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
): ChosenStreams? =
|
): ChosenStreams? =
|
||||||
serverRepository.currentUser?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
|
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
|
||||||
if (itemPlayback != null) {
|
if (itemPlayback != null) {
|
||||||
Timber.v("Got itemPlayback for %s", itemId)
|
Timber.v("Got itemPlayback for %s", itemId)
|
||||||
|
|
@ -72,7 +72,7 @@ class ItemPlaybackRepository
|
||||||
sourceId: UUID,
|
sourceId: UUID,
|
||||||
): ItemPlayback? =
|
): ItemPlayback? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
serverRepository.currentUser?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
val itemPlayback =
|
val itemPlayback =
|
||||||
ItemPlayback(
|
ItemPlayback(
|
||||||
userId = user.rowId,
|
userId = user.rowId,
|
||||||
|
|
@ -89,7 +89,7 @@ class ItemPlaybackRepository
|
||||||
itemPlayback: ItemPlayback?,
|
itemPlayback: ItemPlayback?,
|
||||||
trackIndex: Int,
|
trackIndex: Int,
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
) = serverRepository.currentUser?.let { user ->
|
) = serverRepository.currentUser.value?.let { user ->
|
||||||
var toSave =
|
var toSave =
|
||||||
itemPlayback ?: ItemPlayback(
|
itemPlayback ?: ItemPlayback(
|
||||||
userId = user.rowId,
|
userId = user.rowId,
|
||||||
|
|
@ -113,7 +113,9 @@ class ItemPlaybackRepository
|
||||||
val toSave =
|
val toSave =
|
||||||
if (itemPlayback.userId < 0) {
|
if (itemPlayback.userId < 0) {
|
||||||
val userRowId =
|
val userRowId =
|
||||||
serverRepository.currentUser?.rowId?.takeIf { it >= 0 }
|
serverRepository.currentUser.value
|
||||||
|
?.rowId
|
||||||
|
?.takeIf { it >= 0 }
|
||||||
?: throw IllegalStateException("Trying to save an ItemPlayback without a user, but there is no current user")
|
?: throw IllegalStateException("Trying to save an ItemPlayback without a user, but there is no current user")
|
||||||
itemPlayback.copy(userId = userRowId)
|
itemPlayback.copy(userId = userRowId)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class NavDrawerItemRepository
|
||||||
val user = serverRepository.currentUser
|
val user = serverRepository.currentUser
|
||||||
val userViews =
|
val userViews =
|
||||||
api.userViewsApi
|
api.userViewsApi
|
||||||
.getUserViews(userId = user?.id)
|
.getUserViews(userId = user.value?.id)
|
||||||
.content.items
|
.content.items
|
||||||
|
|
||||||
val builtins = listOf(NavDrawerItem.Favorites)
|
val builtins = listOf(NavDrawerItem.Favorites)
|
||||||
|
|
@ -46,7 +46,7 @@ class NavDrawerItemRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getFilteredNavDrawerItems(items: List<NavDrawerItem>): List<NavDrawerItem> {
|
suspend fun getFilteredNavDrawerItems(items: List<NavDrawerItem>): List<NavDrawerItem> {
|
||||||
val user = serverRepository.currentUser
|
val user = serverRepository.currentUser.value
|
||||||
val navDrawerPins =
|
val navDrawerPins =
|
||||||
user
|
user
|
||||||
?.let {
|
?.let {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@ package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.map
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -41,12 +42,12 @@ class ServerRepository
|
||||||
) {
|
) {
|
||||||
private val sharedPreferences = getServerSharedPreferences(context)
|
private val sharedPreferences = getServerSharedPreferences(context)
|
||||||
|
|
||||||
private var _currentServer by mutableStateOf<JellyfinServer?>(null)
|
private var _current = MutableLiveData<CurrentUser?>(null)
|
||||||
val currentServer get() = _currentServer
|
val current: LiveData<CurrentUser?> = _current
|
||||||
private var _currentUser by mutableStateOf<JellyfinUser?>(null)
|
|
||||||
val currentUser get() = _currentUser
|
val currentServer: LiveData<JellyfinServer?> get() = _current.map { it?.server }
|
||||||
private var _currentUserDto by mutableStateOf<UserDto?>(null)
|
val currentUser: LiveData<JellyfinUser?> get() = _current.map { it?.user }
|
||||||
val currentUserDto get() = _currentUserDto
|
val currentUserDto: LiveData<UserDto?> get() = _current.map { it?.userDto }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a server to the app database and updated the [ApiClient] to the server's URL
|
* Adds a server to the app database and updated the [ApiClient] to the server's URL
|
||||||
|
|
@ -57,19 +58,8 @@ class ServerRepository
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
serverDao.addOrUpdateServer(server)
|
serverDao.addOrUpdateServer(server)
|
||||||
}
|
}
|
||||||
changeServer(server)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the [ApiClient] to the server's URL
|
|
||||||
*
|
|
||||||
* The current user is removed
|
|
||||||
*/
|
|
||||||
fun changeServer(server: JellyfinServer) {
|
|
||||||
apiClient.update(baseUrl = server.url, accessToken = null)
|
apiClient.update(baseUrl = server.url, accessToken = null)
|
||||||
_currentServer = server
|
_current.setValueOnMain(null)
|
||||||
_currentUser = null
|
|
||||||
_currentUserDto = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -109,9 +99,7 @@ class ServerRepository
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_currentUserDto = userDto
|
_current.value = CurrentUser(updatedServer, updatedUser, userDto)
|
||||||
_currentServer = updatedServer
|
|
||||||
_currentUser = updatedUser
|
|
||||||
}
|
}
|
||||||
sharedPreferences.edit(true) {
|
sharedPreferences.edit(true) {
|
||||||
putString(SERVER_URL_KEY, updatedServer.url)
|
putString(SERVER_URL_KEY, updatedServer.url)
|
||||||
|
|
@ -127,6 +115,7 @@ class ServerRepository
|
||||||
userId: UUID?,
|
userId: UUID?,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (serverId == null || userId == null) {
|
if (serverId == null || userId == null) {
|
||||||
|
_current.setValueOnMain(null)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val serverAndUsers =
|
val serverAndUsers =
|
||||||
|
|
@ -185,9 +174,10 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeUser(user: JellyfinUser) {
|
suspend fun removeUser(user: JellyfinUser) {
|
||||||
if (currentUser == user) {
|
if (currentUser.value?.id == user.id) {
|
||||||
_currentUser = null
|
withContext(Dispatchers.Main) {
|
||||||
_currentUserDto = null
|
_current.value = null
|
||||||
|
}
|
||||||
userPreferencesDataStore.updateData {
|
userPreferencesDataStore.updateData {
|
||||||
it
|
it
|
||||||
.toBuilder()
|
.toBuilder()
|
||||||
|
|
@ -203,10 +193,10 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeServer(server: JellyfinServer) {
|
suspend fun removeServer(server: JellyfinServer) {
|
||||||
if (currentServer == server) {
|
if (currentServer.value?.id == server.id) {
|
||||||
_currentServer = null
|
withContext(Dispatchers.Main) {
|
||||||
_currentUser = null
|
_current.value = null
|
||||||
_currentUserDto = null
|
}
|
||||||
userPreferencesDataStore.updateData {
|
userPreferencesDataStore.updateData {
|
||||||
it
|
it
|
||||||
.toBuilder()
|
.toBuilder()
|
||||||
|
|
@ -222,6 +212,18 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun switchServerOrUser() {
|
||||||
|
apiClient.update(baseUrl = null, accessToken = null)
|
||||||
|
userPreferencesDataStore.updateData {
|
||||||
|
it
|
||||||
|
.toBuilder()
|
||||||
|
.apply {
|
||||||
|
currentServerId = ""
|
||||||
|
currentUserId = ""
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getServerSharedPreferences(context: Context): SharedPreferences =
|
fun getServerSharedPreferences(context: Context): SharedPreferences =
|
||||||
context.getSharedPreferences(
|
context.getSharedPreferences(
|
||||||
|
|
@ -233,3 +235,9 @@ class ServerRepository
|
||||||
const val ACCESS_TOKEN_KEY = "current.accessToken"
|
const val ACCESS_TOKEN_KEY = "current.accessToken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class CurrentUser(
|
||||||
|
val server: JellyfinServer,
|
||||||
|
val user: JellyfinUser,
|
||||||
|
val userDto: UserDto,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BaseItem(
|
data class BaseItem(
|
||||||
|
|
@ -18,29 +19,29 @@ data class BaseItem(
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val backdropImageUrl: String? = null,
|
val backdropImageUrl: String? = null,
|
||||||
) {
|
) {
|
||||||
@Transient val id = data.id
|
val id get() = data.id
|
||||||
|
|
||||||
@Transient val type = data.type
|
val type get() = data.type
|
||||||
|
|
||||||
@Transient val name = data.name
|
val name get() = data.name
|
||||||
|
|
||||||
@Transient
|
val title get() = if (type == BaseItemKind.EPISODE) data.seriesName else name
|
||||||
val title = if (type == BaseItemKind.EPISODE) data.seriesName else name
|
|
||||||
|
|
||||||
@Transient
|
val subtitle
|
||||||
val subtitle =
|
get() =
|
||||||
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
|
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
|
||||||
|
|
||||||
@Transient
|
|
||||||
val resumeMs =
|
|
||||||
data.userData
|
|
||||||
?.playbackPositionTicks
|
|
||||||
?.ticks
|
|
||||||
?.inWholeMilliseconds
|
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
val indexNumber = data.indexNumber ?: dateAsIndex()
|
val indexNumber = data.indexNumber ?: dateAsIndex()
|
||||||
|
|
||||||
|
val playbackPosition get() = data.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||||
|
|
||||||
|
val resumeMs get() = playbackPosition.inWholeMilliseconds
|
||||||
|
|
||||||
|
val played get() = data.userData?.played ?: false
|
||||||
|
|
||||||
|
val favorite get() = data.userData?.isFavorite ?: false
|
||||||
|
|
||||||
private fun dateAsIndex(): Int? =
|
private fun dateAsIndex(): Int? =
|
||||||
data.premiereDate
|
data.premiereDate
|
||||||
?.let {
|
?.let {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@file:UseSerializers(UUIDSerializer::class)
|
||||||
|
|
||||||
package com.github.damontecres.wholphin.data.model
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
|
|
@ -8,9 +10,13 @@ import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Entity(tableName = "servers")
|
@Entity(tableName = "servers")
|
||||||
|
@Serializable
|
||||||
data class JellyfinServer(
|
data class JellyfinServer(
|
||||||
@PrimaryKey val id: UUID,
|
@PrimaryKey val id: UUID,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ class PlaylistCreator
|
||||||
name: String,
|
name: String,
|
||||||
initialItems: List<UUID>,
|
initialItems: List<UUID>,
|
||||||
): UUID? =
|
): UUID? =
|
||||||
serverRepository.currentUser?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
api.playlistsApi
|
api.playlistsApi
|
||||||
.createPlaylist(
|
.createPlaylist(
|
||||||
CreatePlaylistDto(
|
CreatePlaylistDto(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManagerImpl
|
||||||
import com.github.damontecres.wholphin.util.RememberTabManager
|
import com.github.damontecres.wholphin.util.RememberTabManager
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -23,6 +25,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.jellyfin.sdk.Jellyfin
|
import org.jellyfin.sdk.Jellyfin
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.util.AuthorizationHeaderBuilder
|
import org.jellyfin.sdk.api.client.util.AuthorizationHeaderBuilder
|
||||||
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
||||||
import org.jellyfin.sdk.createJellyfin
|
import org.jellyfin.sdk.createJellyfin
|
||||||
|
|
@ -92,7 +95,7 @@ object AppModule {
|
||||||
.addInterceptor {
|
.addInterceptor {
|
||||||
val request = it.request()
|
val request = it.request()
|
||||||
val newRequest =
|
val newRequest =
|
||||||
serverRepository.currentUser?.accessToken?.let { token ->
|
serverRepository.currentUser.value?.accessToken?.let { token ->
|
||||||
request
|
request
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addHeader(
|
.addHeader(
|
||||||
|
|
@ -102,7 +105,7 @@ object AppModule {
|
||||||
clientVersion = clientInfo.version,
|
clientVersion = clientInfo.version,
|
||||||
deviceId = deviceInfo.id,
|
deviceId = deviceInfo.id,
|
||||||
deviceName = deviceInfo.name,
|
deviceName = deviceInfo.name,
|
||||||
accessToken = serverRepository.currentUser?.accessToken,
|
accessToken = token,
|
||||||
),
|
),
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +149,7 @@ object AppModule {
|
||||||
appPreference: DataStore<AppPreferences>,
|
appPreference: DataStore<AppPreferences>,
|
||||||
@IoCoroutineScope scope: CoroutineScope,
|
@IoCoroutineScope scope: CoroutineScope,
|
||||||
) = object : RememberTabManager {
|
) = object : RememberTabManager {
|
||||||
fun key(itemId: String) = "${serverRepository.currentServer?.id}_${serverRepository.currentUser?.id}_$itemId"
|
fun key(itemId: String) = "${serverRepository.currentServer.value?.id}_${serverRepository.currentUser.value?.id}_$itemId"
|
||||||
|
|
||||||
override fun getRememberedTab(
|
override fun getRememberedTab(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
|
|
@ -182,4 +185,8 @@ object AppModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
@IoCoroutineScope
|
@IoCoroutineScope
|
||||||
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun favoriteWatchManager(api: ApiClient): FavoriteWatchManager = FavoriteWatchManagerImpl(api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.acra.ACRA
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -380,3 +381,12 @@ fun equalsNotNull(
|
||||||
a: Any?,
|
a: Any?,
|
||||||
b: Any?,
|
b: Any?,
|
||||||
) = a != null && b != null && a == b
|
) = a != null && b != null && a == b
|
||||||
|
|
||||||
|
fun logTab(
|
||||||
|
name: String,
|
||||||
|
tabIndex: Int,
|
||||||
|
) {
|
||||||
|
val info = "$tabIndex, $name"
|
||||||
|
Timber.i("Current tab: $info")
|
||||||
|
ACRA.errorReporter.putCustomData("tabInfo", info)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,21 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.Card
|
import androidx.tv.material3.Card
|
||||||
import androidx.tv.material3.CardDefaults
|
import androidx.tv.material3.CardDefaults
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,6 +50,7 @@ fun BannerCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
cornerText: String? = null,
|
cornerText: String? = null,
|
||||||
played: Boolean = false,
|
played: Boolean = false,
|
||||||
|
favorite: Boolean = false,
|
||||||
playPercent: Double = 0.0,
|
playPercent: Double = 0.0,
|
||||||
cardHeight: Dp = 140.dp * .85f,
|
cardHeight: Dp = 140.dp * .85f,
|
||||||
aspectRatio: Float = 16f / 9,
|
aspectRatio: Float = 16f / 9,
|
||||||
|
|
@ -118,6 +124,18 @@ fun BannerCard(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (favorite) {
|
||||||
|
Text(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(8.dp),
|
||||||
|
color = colorResource(android.R.color.holo_red_light),
|
||||||
|
text = stringResource(R.string.fa_heart),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontAwesome,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (playPercent > 0 && playPercent < 100) {
|
if (playPercent > 0 && playPercent < 100) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ import com.github.damontecres.wholphin.util.FocusPair
|
||||||
fun ItemRow(
|
fun ItemRow(
|
||||||
title: String,
|
title: String,
|
||||||
items: List<BaseItem?>,
|
items: List<BaseItem?>,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||||
cardContent: @Composable (
|
cardContent: @Composable (
|
||||||
index: Int,
|
index: Int,
|
||||||
item: BaseItem?,
|
item: BaseItem?,
|
||||||
|
|
@ -83,8 +83,8 @@ fun ItemRow(
|
||||||
index,
|
index,
|
||||||
item,
|
item,
|
||||||
cardModifier,
|
cardModifier,
|
||||||
{ if (item != null) onClickItem.invoke(item) },
|
{ if (item != null) onClickItem.invoke(index, item) },
|
||||||
{ if (item != null) onLongClickItem.invoke(item) },
|
{ if (item != null) onLongClickItem.invoke(index, item) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,8 +95,8 @@ fun ItemRow(
|
||||||
fun BannerItemRow(
|
fun BannerItemRow(
|
||||||
title: String,
|
title: String,
|
||||||
items: List<BaseItem?>,
|
items: List<BaseItem?>,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
focusPair: FocusPair? = null,
|
focusPair: FocusPair? = null,
|
||||||
cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null,
|
cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ fun PersonRow(
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
onClick: (Person) -> Unit,
|
onClick: (Person) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onLongClick: ((Person) -> Unit)? = null,
|
onLongClick: ((Int, Person) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val firstFocus = remember { FocusRequester() }
|
val firstFocus = remember { FocusRequester() }
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -54,7 +54,7 @@ fun PersonRow(
|
||||||
PersonCard(
|
PersonCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = { onClick.invoke(item) },
|
onClick = { onClick.invoke(item) },
|
||||||
onLongClick = { onLongClick?.invoke(item) },
|
onLongClick = { onLongClick?.invoke(index, item) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.width(120.dp)
|
.width(120.dp)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -37,12 +38,20 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
||||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -57,6 +66,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
|
|
@ -71,6 +81,8 @@ class CollectionFolderViewModel
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
||||||
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
val navigationManager: NavigationManager,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
@ -96,7 +108,7 @@ class CollectionFolderViewModel
|
||||||
|
|
||||||
val sortAndDirection =
|
val sortAndDirection =
|
||||||
if (initialSortAndDirection == null) {
|
if (initialSortAndDirection == null) {
|
||||||
serverRepository.currentUser?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
|
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
|
||||||
} ?: SortAndDirection.DEFAULT
|
} ?: SortAndDirection.DEFAULT
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -111,7 +123,7 @@ class CollectionFolderViewModel
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
filter: GetItemsFilter,
|
filter: GetItemsFilter,
|
||||||
) {
|
) {
|
||||||
serverRepository.currentUser?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val libraryDisplayInfo =
|
val libraryDisplayInfo =
|
||||||
LibraryDisplayInfo(
|
LibraryDisplayInfo(
|
||||||
|
|
@ -228,6 +240,24 @@ class CollectionFolderViewModel
|
||||||
result.totalRecordCount
|
result.totalRecordCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setWatched(
|
||||||
|
position: Int,
|
||||||
|
itemId: UUID,
|
||||||
|
played: Boolean,
|
||||||
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
|
(pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFavorite(
|
||||||
|
position: Int,
|
||||||
|
itemId: UUID,
|
||||||
|
favorite: Boolean,
|
||||||
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
|
(pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -241,7 +271,7 @@ fun CollectionFolderGrid(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
initialFilter: GetItemsFilter,
|
initialFilter: GetItemsFilter,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
sortOptions: List<ItemSortBy>,
|
sortOptions: List<ItemSortBy>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
initialSortAndDirection: SortAndDirection? = null,
|
initialSortAndDirection: SortAndDirection? = null,
|
||||||
|
|
@ -266,14 +296,16 @@ fun CollectionFolderGrid(
|
||||||
itemId: String,
|
itemId: String,
|
||||||
initialFilter: GetItemsFilter,
|
initialFilter: GetItemsFilter,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
sortOptions: List<ItemSortBy>,
|
sortOptions: List<ItemSortBy>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: CollectionFolderViewModel = hiltViewModel(),
|
viewModel: CollectionFolderViewModel = hiltViewModel(),
|
||||||
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
initialSortAndDirection: SortAndDirection? = null,
|
initialSortAndDirection: SortAndDirection? = null,
|
||||||
showTitle: Boolean = true,
|
showTitle: Boolean = true,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
viewModel.init(itemId, initialSortAndDirection, recursive, initialFilter)
|
viewModel.init(itemId, initialSortAndDirection, recursive, initialFilter)
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +315,10 @@ fun CollectionFolderGrid(
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
val pager by viewModel.pager.observeAsState()
|
val pager by viewModel.pager.observeAsState()
|
||||||
|
|
||||||
|
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) }
|
||||||
|
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||||
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
LoadingState.Loading,
|
LoadingState.Loading,
|
||||||
|
|
@ -297,6 +333,9 @@ fun CollectionFolderGrid(
|
||||||
sortAndDirection = sortAndDirection!!,
|
sortAndDirection = sortAndDirection!!,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
|
onLongClickItem = { position, item ->
|
||||||
|
moreDialog.makePresent(PositionItem(position, item))
|
||||||
|
},
|
||||||
onSortChange = {
|
onSortChange = {
|
||||||
viewModel.onSortChange(it, recursive, filter)
|
viewModel.onSortChange(it, recursive, filter)
|
||||||
},
|
},
|
||||||
|
|
@ -308,6 +347,55 @@ fun CollectionFolderGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
moreDialog.compose { (position, item) ->
|
||||||
|
DialogPopup(
|
||||||
|
showDialog = true,
|
||||||
|
title = item.title ?: "",
|
||||||
|
dialogItems =
|
||||||
|
buildMoreDialogItemsForHome(
|
||||||
|
context = context,
|
||||||
|
item = item,
|
||||||
|
seriesId = null,
|
||||||
|
playbackPosition = item.playbackPosition,
|
||||||
|
watched = item.played,
|
||||||
|
favorite = item.favorite,
|
||||||
|
actions =
|
||||||
|
MoreDialogActions(
|
||||||
|
navigateTo = { viewModel.navigationManager.navigateTo(it) },
|
||||||
|
onClickWatch = { itemId, watched ->
|
||||||
|
viewModel.setWatched(position, itemId, watched)
|
||||||
|
},
|
||||||
|
onClickFavorite = { itemId, watched ->
|
||||||
|
viewModel.setFavorite(position, itemId, watched)
|
||||||
|
},
|
||||||
|
onClickAddPlaylist = {
|
||||||
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
|
showPlaylistDialog.makePresent(it)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissRequest = { moreDialog.makeAbsent() },
|
||||||
|
dismissOnClick = true,
|
||||||
|
waitToLoad = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showPlaylistDialog.compose { itemId ->
|
||||||
|
PlaylistDialog(
|
||||||
|
title = stringResource(R.string.add_to_playlist),
|
||||||
|
state = playlistState,
|
||||||
|
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||||
|
onClick = {
|
||||||
|
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
createEnabled = true,
|
||||||
|
onCreatePlaylist = {
|
||||||
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -316,7 +404,8 @@ fun CollectionFolderGridContent(
|
||||||
item: BaseItem?,
|
item: BaseItem?,
|
||||||
pager: List<BaseItem?>,
|
pager: List<BaseItem?>,
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
|
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||||
onSortChange: (SortAndDirection) -> Unit,
|
onSortChange: (SortAndDirection) -> Unit,
|
||||||
letterPosition: suspend (Char) -> Int,
|
letterPosition: suspend (Char) -> Int,
|
||||||
sortOptions: List<ItemSortBy>,
|
sortOptions: List<ItemSortBy>,
|
||||||
|
|
@ -358,7 +447,7 @@ fun CollectionFolderGridContent(
|
||||||
CardGrid(
|
CardGrid(
|
||||||
pager = pager,
|
pager = pager,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = {},
|
onLongClickItem = onLongClickItem,
|
||||||
letterPosition = letterPosition,
|
letterPosition = letterPosition,
|
||||||
gridFocusRequester = gridFocusRequester,
|
gridFocusRequester = gridFocusRequester,
|
||||||
showJumpButtons = false, // TODO add preference
|
showJumpButtons = false, // TODO add preference
|
||||||
|
|
@ -372,3 +461,8 @@ fun CollectionFolderGridContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class PositionItem(
|
||||||
|
val position: Int,
|
||||||
|
val item: BaseItem,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ fun GenreCardGrid(
|
||||||
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
|
||||||
CardGrid(
|
CardGrid(
|
||||||
pager = genres,
|
pager = genres,
|
||||||
onClickItem = { genre ->
|
onClickItem = { _, genre ->
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
Destination.FilteredCollection(
|
Destination.FilteredCollection(
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
|
|
@ -112,7 +112,7 @@ fun GenreCardGrid(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onLongClickItem = {},
|
onLongClickItem = { _, _ -> },
|
||||||
letterPosition = { viewModel.positionOfLetter(it) },
|
letterPosition = { viewModel.positionOfLetter(it) },
|
||||||
gridFocusRequester = gridFocusRequester,
|
gridFocusRequester = gridFocusRequester,
|
||||||
showJumpButtons = false,
|
showJumpButtons = false,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ fun LoadingRow(
|
||||||
rowIndex: Int,
|
rowIndex: Int,
|
||||||
position: RowColumn,
|
position: RowColumn,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
onClickPosition: (RowColumn) -> Unit,
|
onClickPosition: (RowColumn) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showIfEmpty: Boolean = true,
|
showIfEmpty: Boolean = true,
|
||||||
|
|
@ -82,7 +82,7 @@ fun LoadingRow(
|
||||||
title = title,
|
title = title,
|
||||||
items = r.items,
|
items = r.items,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = {},
|
onLongClickItem = { _, _ -> },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
cardContent = cardContent,
|
cardContent = cardContent,
|
||||||
horizontalPadding = horizontalPadding,
|
horizontalPadding = horizontalPadding,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
|
||||||
|
class Optional<T> private constructor(
|
||||||
|
initialValue: T?,
|
||||||
|
) {
|
||||||
|
private var value by mutableStateOf(initialValue)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun compose(run: @Composable (T) -> Unit) {
|
||||||
|
value?.let { run.invoke(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeAbsent() {
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makePresent(value: T) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> absent() = Optional<T>(null)
|
||||||
|
|
||||||
|
fun <T> present(value: T) = Optional(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
abstract class RecommendedViewModel(
|
||||||
|
val navigationManager: NavigationManager,
|
||||||
|
val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
) : ViewModel() {
|
||||||
|
abstract fun init()
|
||||||
|
|
||||||
|
abstract val rows: MutableStateFlow<MutableList<HomeRowLoadingState>>
|
||||||
|
|
||||||
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
|
|
||||||
|
fun refreshItem(
|
||||||
|
position: RowColumn,
|
||||||
|
itemId: UUID,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val row = rows.value.getOrNull(position.row)
|
||||||
|
if (row is HomeRowLoadingState.Success) {
|
||||||
|
(row.items as? ApiRequestPager<*>)?.refreshItem(position.column, itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWatched(
|
||||||
|
position: RowColumn,
|
||||||
|
itemId: UUID,
|
||||||
|
watched: Boolean,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
favoriteWatchManager.setWatched(itemId, watched)
|
||||||
|
refreshItem(position, itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFavorite(
|
||||||
|
position: RowColumn,
|
||||||
|
itemId: UUID,
|
||||||
|
watched: Boolean,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
favoriteWatchManager.setFavorite(itemId, watched)
|
||||||
|
refreshItem(position, itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecommendedContent(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
viewModel: RecommendedViewModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
|
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var moreDialog by remember { mutableStateOf<Optional<RowColumnItem>>(Optional.absent()) }
|
||||||
|
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||||
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
|
|
||||||
|
OneTimeLaunchedEffect {
|
||||||
|
viewModel.init()
|
||||||
|
}
|
||||||
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
val rows by viewModel.rows.collectAsState()
|
||||||
|
|
||||||
|
when (val state = loading) {
|
||||||
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> LoadingPage()
|
||||||
|
|
||||||
|
LoadingState.Success ->
|
||||||
|
HomePageContent(
|
||||||
|
homeRows = rows,
|
||||||
|
onClickItem = { _, item ->
|
||||||
|
viewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
},
|
||||||
|
onLongClickItem = { position, item ->
|
||||||
|
moreDialog.makePresent(RowColumnItem(position, item))
|
||||||
|
},
|
||||||
|
onFocusPosition = onFocusPosition,
|
||||||
|
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
moreDialog.compose { (position, item) ->
|
||||||
|
DialogPopup(
|
||||||
|
showDialog = true,
|
||||||
|
title = item.title ?: "",
|
||||||
|
dialogItems =
|
||||||
|
buildMoreDialogItemsForHome(
|
||||||
|
context = context,
|
||||||
|
item = item,
|
||||||
|
seriesId = null,
|
||||||
|
playbackPosition = item.playbackPosition,
|
||||||
|
watched = item.played,
|
||||||
|
favorite = item.favorite,
|
||||||
|
actions =
|
||||||
|
MoreDialogActions(
|
||||||
|
navigateTo = { viewModel.navigationManager.navigateTo(it) },
|
||||||
|
onClickWatch = { itemId, watched ->
|
||||||
|
viewModel.setWatched(position, itemId, watched)
|
||||||
|
},
|
||||||
|
onClickFavorite = { itemId, watched ->
|
||||||
|
viewModel.setFavorite(position, itemId, watched)
|
||||||
|
},
|
||||||
|
onClickAddPlaylist = {
|
||||||
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
|
showPlaylistDialog.makePresent(it)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissRequest = { moreDialog.makeAbsent() },
|
||||||
|
dismissOnClick = true,
|
||||||
|
waitToLoad = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showPlaylistDialog.compose { itemId ->
|
||||||
|
PlaylistDialog(
|
||||||
|
title = stringResource(R.string.add_to_playlist),
|
||||||
|
state = playlistState,
|
||||||
|
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||||
|
onClick = {
|
||||||
|
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
createEnabled = true,
|
||||||
|
onCreatePlaylist = {
|
||||||
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class RowColumnItem(
|
||||||
|
val position: RowColumn,
|
||||||
|
val item: BaseItem,
|
||||||
|
)
|
||||||
|
|
@ -2,25 +2,19 @@ package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
||||||
|
|
@ -54,15 +48,15 @@ class RecommendedMovieViewModel
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
@Assisted val parentId: UUID,
|
@Assisted val parentId: UUID,
|
||||||
) : ViewModel() {
|
navigationManager: NavigationManager,
|
||||||
|
favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
) : RecommendedViewModel(navigationManager, favoriteWatchManager) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(parentId: UUID): RecommendedMovieViewModel
|
fun create(parentId: UUID): RecommendedMovieViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
override val rows =
|
||||||
|
|
||||||
val rows =
|
|
||||||
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
||||||
rowTitles
|
rowTitles
|
||||||
.map {
|
.map {
|
||||||
|
|
@ -72,7 +66,7 @@ class RecommendedMovieViewModel
|
||||||
}.toMutableList(),
|
}.toMutableList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun init() {
|
override fun init() {
|
||||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
try {
|
try {
|
||||||
val resumeItemsRequest =
|
val resumeItemsRequest =
|
||||||
|
|
@ -139,7 +133,7 @@ class RecommendedMovieViewModel
|
||||||
|
|
||||||
val suggestionsRequest =
|
val suggestionsRequest =
|
||||||
GetSuggestionsRequest(
|
GetSuggestionsRequest(
|
||||||
userId = serverRepository.currentUser?.id,
|
userId = serverRepository.currentUser.value?.id,
|
||||||
type = listOf(BaseItemKind.MOVIE),
|
type = listOf(BaseItemKind.MOVIE),
|
||||||
)
|
)
|
||||||
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)
|
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)
|
||||||
|
|
@ -211,7 +205,6 @@ class RecommendedMovieViewModel
|
||||||
fun RecommendedMovie(
|
fun RecommendedMovie(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
parentId: UUID,
|
parentId: UUID,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
|
||||||
onFocusPosition: (RowColumn) -> Unit,
|
onFocusPosition: (RowColumn) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: RecommendedMovieViewModel =
|
viewModel: RecommendedMovieViewModel =
|
||||||
|
|
@ -219,27 +212,10 @@ fun RecommendedMovie(
|
||||||
creationCallback = { it.create(parentId) },
|
creationCallback = { it.create(parentId) },
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
RecommendedContent(
|
||||||
viewModel.init()
|
preferences = preferences,
|
||||||
}
|
viewModel = viewModel,
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
|
||||||
val rows by viewModel.rows.collectAsState()
|
|
||||||
|
|
||||||
when (val state = loading) {
|
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
|
||||||
|
|
||||||
LoadingState.Loading,
|
|
||||||
LoadingState.Pending,
|
|
||||||
-> LoadingPage()
|
|
||||||
|
|
||||||
LoadingState.Success ->
|
|
||||||
HomePageContent(
|
|
||||||
homeRows = rows,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = {},
|
|
||||||
onFocusPosition = onFocusPosition,
|
onFocusPosition = onFocusPosition,
|
||||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,19 @@ package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetNextUpRequestHandler
|
import com.github.damontecres.wholphin.util.GetNextUpRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
||||||
|
|
@ -56,15 +50,15 @@ class RecommendedTvShowViewModel
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
@Assisted val parentId: UUID,
|
@Assisted val parentId: UUID,
|
||||||
) : ViewModel() {
|
navigationManager: NavigationManager,
|
||||||
|
favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
) : RecommendedViewModel(navigationManager, favoriteWatchManager) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(parentId: UUID): RecommendedTvShowViewModel
|
fun create(parentId: UUID): RecommendedTvShowViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
override val rows =
|
||||||
|
|
||||||
val rows =
|
|
||||||
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
||||||
rowTitles
|
rowTitles
|
||||||
.map {
|
.map {
|
||||||
|
|
@ -74,7 +68,7 @@ class RecommendedTvShowViewModel
|
||||||
}.toMutableList(),
|
}.toMutableList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun init() {
|
override fun init() {
|
||||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
try {
|
try {
|
||||||
val resumeItemsRequest =
|
val resumeItemsRequest =
|
||||||
|
|
@ -167,7 +161,7 @@ class RecommendedTvShowViewModel
|
||||||
|
|
||||||
val suggestionsRequest =
|
val suggestionsRequest =
|
||||||
GetSuggestionsRequest(
|
GetSuggestionsRequest(
|
||||||
userId = serverRepository.currentUser?.id,
|
userId = serverRepository.currentUser.value?.id,
|
||||||
type = listOf(BaseItemKind.SERIES),
|
type = listOf(BaseItemKind.SERIES),
|
||||||
)
|
)
|
||||||
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)
|
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)
|
||||||
|
|
@ -240,7 +234,6 @@ class RecommendedTvShowViewModel
|
||||||
fun RecommendedTvShow(
|
fun RecommendedTvShow(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
parentId: UUID,
|
parentId: UUID,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
|
||||||
onFocusPosition: (RowColumn) -> Unit,
|
onFocusPosition: (RowColumn) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: RecommendedTvShowViewModel =
|
viewModel: RecommendedTvShowViewModel =
|
||||||
|
|
@ -248,28 +241,10 @@ fun RecommendedTvShow(
|
||||||
creationCallback = { it.create(parentId) },
|
creationCallback = { it.create(parentId) },
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
RecommendedContent(
|
||||||
viewModel.init()
|
preferences = preferences,
|
||||||
}
|
viewModel = viewModel,
|
||||||
|
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
|
||||||
val rows by viewModel.rows.collectAsState()
|
|
||||||
|
|
||||||
when (val state = loading) {
|
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
|
||||||
|
|
||||||
LoadingState.Loading,
|
|
||||||
LoadingState.Pending,
|
|
||||||
-> LoadingPage()
|
|
||||||
|
|
||||||
LoadingState.Success ->
|
|
||||||
HomePageContent(
|
|
||||||
homeRows = rows,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onLongClickItem = {},
|
|
||||||
onFocusPosition = onFocusPosition,
|
onFocusPosition = onFocusPosition,
|
||||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ private const val DEBUG = false
|
||||||
@Composable
|
@Composable
|
||||||
fun CardGrid(
|
fun CardGrid(
|
||||||
pager: List<BaseItem?>,
|
pager: List<BaseItem?>,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||||
letterPosition: suspend (Char) -> Int,
|
letterPosition: suspend (Char) -> Int,
|
||||||
gridFocusRequester: FocusRequester,
|
gridFocusRequester: FocusRequester,
|
||||||
showJumpButtons: Boolean,
|
showJumpButtons: Boolean,
|
||||||
|
|
@ -270,10 +270,10 @@ fun CardGrid(
|
||||||
{
|
{
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
onClickItem.invoke(item)
|
onClickItem.invoke(index, item)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ if (item != null) onLongClickItem.invoke(item) },
|
{ if (item != null) onLongClickItem.invoke(index, item) },
|
||||||
mod
|
mod
|
||||||
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
|
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ fun CollectionFolderGeneric(
|
||||||
var showHeader by remember { mutableStateOf(true) }
|
var showHeader by remember { mutableStateOf(true) }
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = { preferencesViewModel.navigationManager.navigateTo(it.destination()) },
|
onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) },
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
initialFilter = filter,
|
initialFilter = filter,
|
||||||
showTitle = showHeader,
|
showTitle = showHeader,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
|
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
|
||||||
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
|
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
|
@ -65,7 +66,7 @@ class LiveTvCollectionViewModel
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val folders =
|
val folders =
|
||||||
api.liveTvApi
|
api.liveTvApi
|
||||||
.getRecordingFolders(userId = serverRepository.currentUser?.id)
|
.getRecordingFolders(userId = serverRepository.currentUser.value?.id)
|
||||||
.content.items
|
.content.items
|
||||||
.map { TabId(it.name ?: "Recordings", it.id) }
|
.map { TabId(it.name ?: "Recordings", it.id) }
|
||||||
this@LiveTvCollectionViewModel.recordingFolders.setValueOnMain(folders)
|
this@LiveTvCollectionViewModel.recordingFolders.setValueOnMain(folders)
|
||||||
|
|
@ -102,9 +103,10 @@ fun CollectionFolderLiveTv(
|
||||||
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
||||||
|
|
||||||
LaunchedEffect(selectedTabIndex) {
|
LaunchedEffect(selectedTabIndex) {
|
||||||
|
logTab("livetv", selectedTabIndex)
|
||||||
viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
|
viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
|
||||||
}
|
}
|
||||||
val onClickItem = { item: BaseItem ->
|
val onClickItem = { position: Int, item: BaseItem ->
|
||||||
viewModel.navigationManager.navigateTo(item.destination())
|
viewModel.navigationManager.navigateTo(item.destination())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||||
|
|
@ -33,6 +32,7 @@ import com.github.damontecres.wholphin.ui.components.RecommendedMovie
|
||||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||||
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -62,15 +62,12 @@ fun CollectionFolderMovie(
|
||||||
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
||||||
|
|
||||||
LaunchedEffect(selectedTabIndex) {
|
LaunchedEffect(selectedTabIndex) {
|
||||||
|
logTab("movie", selectedTabIndex)
|
||||||
preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
|
preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
val onClickItem = { item: BaseItem ->
|
|
||||||
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -94,7 +91,6 @@ fun CollectionFolderMovie(
|
||||||
0 -> {
|
0 -> {
|
||||||
RecommendedMovie(
|
RecommendedMovie(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
|
||||||
parentId = destination.itemId,
|
parentId = destination.itemId,
|
||||||
onFocusPosition = { pos ->
|
onFocusPosition = { pos ->
|
||||||
showHeader = pos.row < 1
|
showHeader = pos.row < 1
|
||||||
|
|
@ -109,7 +105,9 @@ fun CollectionFolderMovie(
|
||||||
1 -> {
|
1 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
onClickItem = { _, item ->
|
||||||
|
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
},
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
initialFilter =
|
initialFilter =
|
||||||
GetItemsFilter(
|
GetItemsFilter(
|
||||||
|
|
@ -131,7 +129,9 @@ fun CollectionFolderMovie(
|
||||||
2 -> {
|
2 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
onClickItem = { _, item ->
|
||||||
|
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
},
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
initialFilter =
|
initialFilter =
|
||||||
GetItemsFilter(
|
GetItemsFilter(
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
|
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
|
||||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||||
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -60,6 +61,7 @@ fun CollectionFolderTv(
|
||||||
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
||||||
|
|
||||||
LaunchedEffect(selectedTabIndex) {
|
LaunchedEffect(selectedTabIndex) {
|
||||||
|
logTab("tv", selectedTabIndex)
|
||||||
preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
|
preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +95,6 @@ fun CollectionFolderTv(
|
||||||
RecommendedTvShow(
|
RecommendedTvShow(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
parentId = destination.itemId,
|
parentId = destination.itemId,
|
||||||
onClickItem = onClickItem,
|
|
||||||
onFocusPosition = { pos ->
|
onFocusPosition = { pos ->
|
||||||
showHeader = pos.row < 1
|
showHeader = pos.row < 1
|
||||||
},
|
},
|
||||||
|
|
@ -123,7 +124,9 @@ fun CollectionFolderTv(
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
onClickItem = onClickItem,
|
onClickItem = { _, item ->
|
||||||
|
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class DebugViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
serverRepository.currentUser?.rowId?.let {
|
serverRepository.currentUser.value?.rowId?.let {
|
||||||
val results = itemPlaybackDao.getItems(it)
|
val results = itemPlaybackDao.getItems(it)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
itemPlaybacks.value = results
|
itemPlaybacks.value = results
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -20,6 +21,13 @@ import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
data class MoreDialogActions(
|
||||||
|
val navigateTo: (Destination) -> Unit,
|
||||||
|
var onClickWatch: (UUID, Boolean) -> Unit,
|
||||||
|
var onClickFavorite: (UUID, Boolean) -> Unit,
|
||||||
|
var onClickAddPlaylist: (UUID) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the [DialogItem]s when clicking "More"
|
* Build the [DialogItem]s when clicking "More"
|
||||||
*
|
*
|
||||||
|
|
@ -43,12 +51,9 @@ fun buildMoreDialogItems(
|
||||||
sourceId: UUID?,
|
sourceId: UUID?,
|
||||||
watched: Boolean,
|
watched: Boolean,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
navigateTo: (Destination) -> Unit,
|
actions: MoreDialogActions,
|
||||||
onClickWatch: (Boolean) -> Unit,
|
|
||||||
onClickFavorite: (Boolean) -> Unit,
|
|
||||||
onChooseVersion: () -> Unit,
|
onChooseVersion: () -> Unit,
|
||||||
onChooseTracks: (MediaStreamType) -> Unit,
|
onChooseTracks: (MediaStreamType) -> Unit,
|
||||||
onClickAddPlaylist: () -> Unit,
|
|
||||||
): List<DialogItem> =
|
): List<DialogItem> =
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
|
|
@ -57,7 +62,7 @@ fun buildMoreDialogItems(
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
actions.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
item.id,
|
item.id,
|
||||||
item.resumeMs ?: 0L,
|
item.resumeMs ?: 0L,
|
||||||
|
|
@ -119,7 +124,7 @@ fun buildMoreDialogItems(
|
||||||
text = R.string.add_to_playlist,
|
text = R.string.add_to_playlist,
|
||||||
iconStringRes = R.string.fa_list_ul,
|
iconStringRes = R.string.fa_list_ul,
|
||||||
) {
|
) {
|
||||||
onClickAddPlaylist.invoke()
|
actions.onClickAddPlaylist.invoke(item.id)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
|
|
@ -127,7 +132,7 @@ fun buildMoreDialogItems(
|
||||||
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
||||||
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
|
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
|
||||||
) {
|
) {
|
||||||
onClickWatch.invoke(!watched)
|
actions.onClickWatch.invoke(item.id, !watched)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
|
|
@ -136,7 +141,7 @@ fun buildMoreDialogItems(
|
||||||
iconStringRes = R.string.fa_heart,
|
iconStringRes = R.string.fa_heart,
|
||||||
iconColor = if (favorite) Color.Red else Color.Unspecified,
|
iconColor = if (favorite) Color.Red else Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
onClickFavorite.invoke(!favorite)
|
actions.onClickFavorite.invoke(item.id, !favorite)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
series?.let {
|
series?.let {
|
||||||
|
|
@ -145,7 +150,7 @@ fun buildMoreDialogItems(
|
||||||
context.getString(R.string.go_to_series),
|
context.getString(R.string.go_to_series),
|
||||||
Icons.AutoMirrored.Filled.ArrowForward,
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
actions.navigateTo(
|
||||||
Destination.MediaItem(
|
Destination.MediaItem(
|
||||||
series.id,
|
series.id,
|
||||||
BaseItemKind.SERIES,
|
BaseItemKind.SERIES,
|
||||||
|
|
@ -160,7 +165,7 @@ fun buildMoreDialogItems(
|
||||||
context.getString(R.string.play_with_transcoding),
|
context.getString(R.string.play_with_transcoding),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
actions.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
item.id,
|
item.id,
|
||||||
item.resumeMs ?: 0L,
|
item.resumeMs ?: 0L,
|
||||||
|
|
@ -179,10 +184,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
playbackPosition: Duration,
|
playbackPosition: Duration,
|
||||||
watched: Boolean,
|
watched: Boolean,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
navigateTo: (Destination) -> Unit,
|
actions: MoreDialogActions,
|
||||||
onClickWatch: (UUID, Boolean) -> Unit,
|
|
||||||
onClickFavorite: (UUID, Boolean) -> Unit,
|
|
||||||
onClickAddPlaylist: (UUID) -> Unit,
|
|
||||||
): List<DialogItem> =
|
): List<DialogItem> =
|
||||||
buildList {
|
buildList {
|
||||||
val itemId = item.id
|
val itemId = item.id
|
||||||
|
|
@ -191,7 +193,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
context.getString(R.string.go_to),
|
context.getString(R.string.go_to),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
) {
|
) {
|
||||||
navigateTo(item.destination())
|
actions.navigateTo(item.destination())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (item.type in supportedPlayableTypes) {
|
if (item.type in supportedPlayableTypes) {
|
||||||
|
|
@ -202,7 +204,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
actions.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
itemId,
|
itemId,
|
||||||
playbackPosition.inWholeMilliseconds,
|
playbackPosition.inWholeMilliseconds,
|
||||||
|
|
@ -216,7 +218,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
Icons.Default.Refresh,
|
Icons.Default.Refresh,
|
||||||
// iconColor = Color.Green.copy(alpha = .8f),
|
// iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
actions.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
itemId,
|
itemId,
|
||||||
0L,
|
0L,
|
||||||
|
|
@ -231,7 +233,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
actions.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
itemId,
|
itemId,
|
||||||
0L,
|
0L,
|
||||||
|
|
@ -246,7 +248,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
text = R.string.add_to_playlist,
|
text = R.string.add_to_playlist,
|
||||||
iconStringRes = R.string.fa_list_ul,
|
iconStringRes = R.string.fa_list_ul,
|
||||||
) {
|
) {
|
||||||
onClickAddPlaylist.invoke(itemId)
|
actions.onClickAddPlaylist.invoke(itemId)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
|
|
@ -254,7 +256,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
||||||
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
|
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
|
||||||
) {
|
) {
|
||||||
onClickWatch.invoke(itemId, !watched)
|
actions.onClickWatch.invoke(itemId, !watched)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
|
|
@ -263,7 +265,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
iconStringRes = R.string.fa_heart,
|
iconStringRes = R.string.fa_heart,
|
||||||
iconColor = if (favorite) Color.Red else Color.Unspecified,
|
iconColor = if (favorite) Color.Red else Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
onClickFavorite.invoke(itemId, !favorite)
|
actions.onClickFavorite.invoke(itemId, !favorite)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
seriesId?.let {
|
seriesId?.let {
|
||||||
|
|
@ -272,7 +274,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
context.getString(R.string.go_to_series),
|
context.getString(R.string.go_to_series),
|
||||||
Icons.AutoMirrored.Filled.ArrowForward,
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
) {
|
) {
|
||||||
navigateTo(
|
actions.navigateTo(
|
||||||
Destination.MediaItem(
|
Destination.MediaItem(
|
||||||
it,
|
it,
|
||||||
BaseItemKind.SERIES,
|
BaseItemKind.SERIES,
|
||||||
|
|
@ -282,3 +284,31 @@ fun buildMoreDialogItemsForHome(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildMoreDialogItemsForPerson(
|
||||||
|
context: Context,
|
||||||
|
person: Person,
|
||||||
|
// favorite: Boolean,
|
||||||
|
actions: MoreDialogActions,
|
||||||
|
): List<DialogItem> =
|
||||||
|
buildList {
|
||||||
|
val itemId = person.id
|
||||||
|
add(
|
||||||
|
DialogItem(
|
||||||
|
context.getString(R.string.go_to),
|
||||||
|
Icons.Default.ArrowForward,
|
||||||
|
) {
|
||||||
|
actions.navigateTo(Destination.MediaItem(itemId, BaseItemKind.PERSON))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// TODO need way to get person's favorite status
|
||||||
|
// add(
|
||||||
|
// DialogItem(
|
||||||
|
// text = if (favorite) R.string.remove_favorite else R.string.add_favorite,
|
||||||
|
// iconStringRes = R.string.fa_heart,
|
||||||
|
// iconColor = if (favorite) Color.Red else Color.Unspecified,
|
||||||
|
// ) {
|
||||||
|
// actions.onClickFavorite.invoke(itemId, !favorite)
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import com.github.damontecres.wholphin.ui.components.TabRow
|
||||||
import com.github.damontecres.wholphin.ui.data.EpisodeSortOptions
|
import com.github.damontecres.wholphin.ui.data.EpisodeSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||||
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -63,6 +64,7 @@ fun FavoritesPage(
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(selectedTabIndex) {
|
LaunchedEffect(selectedTabIndex) {
|
||||||
|
logTab("favorites", selectedTabIndex)
|
||||||
preferencesViewModel.saveRememberedTab(
|
preferencesViewModel.saveRememberedTab(
|
||||||
preferences,
|
preferences,
|
||||||
NavDrawerItem.Favorites.id,
|
NavDrawerItem.Favorites.id,
|
||||||
|
|
@ -97,7 +99,7 @@ fun FavoritesPage(
|
||||||
0 -> {
|
0 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_movies",
|
itemId = "${NavDrawerItem.Favorites.id}_movies",
|
||||||
initialFilter =
|
initialFilter =
|
||||||
GetItemsFilter(
|
GetItemsFilter(
|
||||||
|
|
@ -120,7 +122,7 @@ fun FavoritesPage(
|
||||||
1 -> {
|
1 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_series",
|
itemId = "${NavDrawerItem.Favorites.id}_series",
|
||||||
initialFilter =
|
initialFilter =
|
||||||
GetItemsFilter(
|
GetItemsFilter(
|
||||||
|
|
@ -143,7 +145,7 @@ fun FavoritesPage(
|
||||||
2 -> {
|
2 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
onClickItem = { _, item -> onClickItem.invoke(item) },
|
||||||
itemId = "${NavDrawerItem.Favorites.id}_episodes",
|
itemId = "${NavDrawerItem.Favorites.id}_episodes",
|
||||||
initialFilter =
|
initialFilter =
|
||||||
GetItemsFilter(
|
GetItemsFilter(
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shadow
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -186,8 +183,8 @@ fun PersonPage(
|
||||||
movies = movies,
|
movies = movies,
|
||||||
series = series,
|
series = series,
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
onClickItem = {
|
onClickItem = { index, item ->
|
||||||
viewModel.navigationManager.navigateTo(it.destination())
|
viewModel.navigationManager.navigateTo(item.destination())
|
||||||
},
|
},
|
||||||
overviewOnClick = { showOverviewDialog = true },
|
overviewOnClick = { showOverviewDialog = true },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -226,7 +223,7 @@ fun PersonPageContent(
|
||||||
movies: RowLoadingState,
|
movies: RowLoadingState,
|
||||||
series: RowLoadingState,
|
series: RowLoadingState,
|
||||||
episodes: RowLoadingState,
|
episodes: RowLoadingState,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,18 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Optional
|
||||||
import com.github.damontecres.wholphin.ui.components.chooseStream
|
import com.github.damontecres.wholphin.ui.components.chooseStream
|
||||||
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
|
@ -83,6 +87,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -112,9 +117,24 @@ fun MovieDetails(
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
var showPlaylistDialog by remember { mutableStateOf(false) }
|
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
|
|
||||||
|
val moreActions =
|
||||||
|
MoreDialogActions(
|
||||||
|
navigateTo = viewModel::navigateTo,
|
||||||
|
onClickWatch = { itemId, watched ->
|
||||||
|
viewModel.setWatched(itemId, watched)
|
||||||
|
},
|
||||||
|
onClickFavorite = { itemId, favorite ->
|
||||||
|
viewModel.setFavorite(itemId, favorite)
|
||||||
|
},
|
||||||
|
onClickAddPlaylist = { itemId ->
|
||||||
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
|
showPlaylistDialog.makePresent(itemId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
LoadingState.Loading,
|
LoadingState.Loading,
|
||||||
|
|
@ -139,8 +159,8 @@ fun MovieDetails(
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
trailers = trailers,
|
trailers = trailers,
|
||||||
similar = similar,
|
similar = similar,
|
||||||
onClickItem = {
|
onClickItem = { index, item ->
|
||||||
viewModel.navigateTo(it.destination())
|
viewModel.navigateTo(item.destination())
|
||||||
},
|
},
|
||||||
onClickPerson = {
|
onClickPerson = {
|
||||||
viewModel.navigateTo(
|
viewModel.navigateTo(
|
||||||
|
|
@ -180,9 +200,7 @@ fun MovieDetails(
|
||||||
favorite = movie.data.userData?.isFavorite ?: false,
|
favorite = movie.data.userData?.isFavorite ?: false,
|
||||||
series = null,
|
series = null,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.sourceId,
|
||||||
navigateTo = viewModel::navigateTo,
|
actions = moreActions,
|
||||||
onClickWatch = viewModel::setWatched,
|
|
||||||
onClickFavorite = viewModel::setFavorite,
|
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseVersionParams(
|
chooseVersionParams(
|
||||||
|
|
@ -218,18 +236,46 @@ fun MovieDetails(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickAddPlaylist = {
|
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
|
||||||
showPlaylistDialog = true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
watchOnClick = {
|
watchOnClick = {
|
||||||
viewModel.setWatched((movie.data.userData?.played ?: false).not())
|
viewModel.setWatched(movie.id, !movie.played)
|
||||||
},
|
},
|
||||||
favoriteOnClick = {
|
favoriteOnClick = {
|
||||||
viewModel.setFavorite((movie.data.userData?.isFavorite ?: false).not())
|
viewModel.setFavorite(movie.id, !movie.favorite)
|
||||||
|
},
|
||||||
|
onLongClickPerson = { index, person ->
|
||||||
|
val items =
|
||||||
|
buildMoreDialogItemsForPerson(
|
||||||
|
context = context,
|
||||||
|
person = person,
|
||||||
|
actions = moreActions,
|
||||||
|
)
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = true,
|
||||||
|
title = person.name ?: "",
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLongClickSimilar = { index, similar ->
|
||||||
|
val items =
|
||||||
|
buildMoreDialogItemsForHome(
|
||||||
|
context = context,
|
||||||
|
item = similar,
|
||||||
|
seriesId = null,
|
||||||
|
playbackPosition = similar.playbackPosition,
|
||||||
|
watched = similar.played,
|
||||||
|
favorite = similar.favorite,
|
||||||
|
actions = moreActions,
|
||||||
|
)
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = true,
|
||||||
|
title = similar.title ?: "",
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
trailerOnClick = { trailer ->
|
trailerOnClick = { trailer ->
|
||||||
when (trailer) {
|
when (trailer) {
|
||||||
|
|
@ -257,7 +303,7 @@ fun MovieDetails(
|
||||||
ItemDetailsDialog(
|
ItemDetailsDialog(
|
||||||
info = info,
|
info = info,
|
||||||
showFilePath =
|
showFilePath =
|
||||||
viewModel.serverRepository.currentUserDto
|
viewModel.serverRepository.currentUserDto.value
|
||||||
?.policy
|
?.policy
|
||||||
?.isAdministrator == true,
|
?.isAdministrator == true,
|
||||||
onDismissRequest = { overviewDialog = null },
|
onDismissRequest = { overviewDialog = null },
|
||||||
|
|
@ -283,19 +329,19 @@ fun MovieDetails(
|
||||||
waitToLoad = params.fromLongClick,
|
waitToLoad = params.fromLongClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showPlaylistDialog) {
|
showPlaylistDialog.compose { itemId ->
|
||||||
PlaylistDialog(
|
PlaylistDialog(
|
||||||
title = stringResource(R.string.add_to_playlist),
|
title = stringResource(R.string.add_to_playlist),
|
||||||
state = playlistState,
|
state = playlistState,
|
||||||
onDismissRequest = { showPlaylistDialog = false },
|
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||||
onClick = {
|
onClick = {
|
||||||
playlistViewModel.addToPlaylist(it.id, destination.itemId)
|
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||||
showPlaylistDialog = false
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
createEnabled = true,
|
createEnabled = true,
|
||||||
onCreatePlaylist = {
|
onCreatePlaylist = {
|
||||||
playlistViewModel.createPlaylistAndAddItem(it, destination.itemId)
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
showPlaylistDialog = false
|
showPlaylistDialog.makeAbsent()
|
||||||
},
|
},
|
||||||
elevation = 3.dp,
|
elevation = 3.dp,
|
||||||
)
|
)
|
||||||
|
|
@ -323,8 +369,10 @@ fun MovieDetailsContent(
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
favoriteOnClick: () -> Unit,
|
favoriteOnClick: () -> Unit,
|
||||||
moreOnClick: () -> Unit,
|
moreOnClick: () -> Unit,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
onClickPerson: (Person) -> Unit,
|
onClickPerson: (Person) -> Unit,
|
||||||
|
onLongClickPerson: (Int, Person) -> Unit,
|
||||||
|
onLongClickSimilar: (Int, BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
@ -422,7 +470,10 @@ fun MovieDetailsContent(
|
||||||
position = PEOPLE_ROW
|
position = PEOPLE_ROW
|
||||||
onClickPerson.invoke(it)
|
onClickPerson.invoke(it)
|
||||||
},
|
},
|
||||||
onLongClick = {},
|
onLongClick = { index, person ->
|
||||||
|
position = PEOPLE_ROW
|
||||||
|
onLongClickPerson.invoke(index, person)
|
||||||
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -466,11 +517,14 @@ fun MovieDetailsContent(
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(R.string.more_like_this),
|
title = stringResource(R.string.more_like_this),
|
||||||
items = similar,
|
items = similar,
|
||||||
onClickItem = {
|
onClickItem = { index, item ->
|
||||||
position = SIMILAR_ROW
|
position = SIMILAR_ROW
|
||||||
onClickItem.invoke(it)
|
onClickItem.invoke(index, item)
|
||||||
|
},
|
||||||
|
onLongClickItem = { index, similar ->
|
||||||
|
position = SIMILAR_ROW
|
||||||
|
onLongClickSimilar.invoke(index, similar)
|
||||||
},
|
},
|
||||||
onLongClickItem = {},
|
|
||||||
cardContent = { index, item, mod, onClick, onLongClick ->
|
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||||
SeasonCard(
|
SeasonCard(
|
||||||
item = item,
|
item = item,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.ThemeSongPlayer
|
import com.github.damontecres.wholphin.util.ThemeSongPlayer
|
||||||
|
|
@ -39,7 +40,6 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||||
|
|
@ -55,6 +55,7 @@ class MovieViewModel
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
private val themeSongPlayer: ThemeSongPlayer,
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
@Assisted val itemId: UUID,
|
@Assisted val itemId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -146,7 +147,7 @@ class MovieViewModel
|
||||||
api.libraryApi
|
api.libraryApi
|
||||||
.getSimilarItems(
|
.getSimilarItems(
|
||||||
GetSimilarItemsRequest(
|
GetSimilarItemsRequest(
|
||||||
userId = serverRepository.currentUser?.id,
|
userId = serverRepository.currentUser.value?.id,
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
fields = SlimItemFields,
|
fields = SlimItemFields,
|
||||||
limit = 25,
|
limit = 25,
|
||||||
|
|
@ -157,23 +158,19 @@ class MovieViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWatched(played: Boolean) =
|
fun setWatched(
|
||||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
itemId: UUID,
|
||||||
if (played) {
|
played: Boolean,
|
||||||
api.playStateApi.markPlayedItem(itemId)
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
} else {
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
api.playStateApi.markUnplayedItem(itemId)
|
|
||||||
}
|
|
||||||
fetchAndSetItem()
|
fetchAndSetItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFavorite(favorite: Boolean) =
|
fun setFavorite(
|
||||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
itemId: UUID,
|
||||||
if (favorite) {
|
favorite: Boolean,
|
||||||
api.userLibraryApi.markFavoriteItem(itemId)
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
} else {
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
|
||||||
}
|
|
||||||
fetchAndSetItem()
|
fetchAndSetItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,17 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
|
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Optional
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
|
||||||
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -74,7 +81,9 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -83,6 +92,7 @@ fun SeriesDetails(
|
||||||
destination: Destination.MediaItem,
|
destination: Destination.MediaItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SeriesViewModel = hiltViewModel(),
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
|
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -98,6 +108,8 @@ fun SeriesDetails(
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
var showWatchConfirmation by remember { mutableStateOf(false) }
|
var showWatchConfirmation by remember { mutableStateOf(false) }
|
||||||
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
|
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||||
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
@ -126,7 +138,9 @@ fun SeriesDetails(
|
||||||
played = played,
|
played = played,
|
||||||
favorite = item.data.userData?.isFavorite ?: false,
|
favorite = item.data.userData?.isFavorite ?: false,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClickItem = { viewModel.navigateTo(it.destination()) },
|
onClickItem = { index, item ->
|
||||||
|
viewModel.navigateTo(item.destination())
|
||||||
|
},
|
||||||
onClickPerson = {
|
onClickPerson = {
|
||||||
viewModel.navigateTo(
|
viewModel.navigateTo(
|
||||||
Destination.MediaItem(
|
Destination.MediaItem(
|
||||||
|
|
@ -135,7 +149,7 @@ fun SeriesDetails(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onLongClickItem = { season ->
|
onLongClickItem = { index, season ->
|
||||||
seasonDialog =
|
seasonDialog =
|
||||||
buildDialogForSeason(
|
buildDialogForSeason(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
@ -160,6 +174,20 @@ fun SeriesDetails(
|
||||||
val favorite = item.data.userData?.isFavorite ?: false
|
val favorite = item.data.userData?.isFavorite ?: false
|
||||||
viewModel.setFavorite(item.id, !favorite, null)
|
viewModel.setFavorite(item.id, !favorite, null)
|
||||||
},
|
},
|
||||||
|
moreActions =
|
||||||
|
MoreDialogActions(
|
||||||
|
navigateTo = { viewModel.navigateTo(it) },
|
||||||
|
onClickWatch = { itemId, played ->
|
||||||
|
viewModel.setWatched(itemId, played, null)
|
||||||
|
},
|
||||||
|
onClickFavorite = { itemId, played ->
|
||||||
|
viewModel.setFavorite(itemId, played, null)
|
||||||
|
},
|
||||||
|
onClickAddPlaylist = { itemId ->
|
||||||
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
|
showPlaylistDialog.makePresent(itemId)
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (showWatchConfirmation) {
|
if (showWatchConfirmation) {
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
|
|
@ -194,6 +222,23 @@ fun SeriesDetails(
|
||||||
onDismissRequest = { seasonDialog = null },
|
onDismissRequest = { seasonDialog = null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
showPlaylistDialog.compose { itemId ->
|
||||||
|
PlaylistDialog(
|
||||||
|
title = stringResource(R.string.add_to_playlist),
|
||||||
|
state = playlistState,
|
||||||
|
onDismissRequest = { showPlaylistDialog.makeAbsent() },
|
||||||
|
onClick = {
|
||||||
|
playlistViewModel.addToPlaylist(it.id, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
createEnabled = true,
|
||||||
|
onCreatePlaylist = {
|
||||||
|
playlistViewModel.createPlaylistAndAddItem(it, itemId)
|
||||||
|
showPlaylistDialog.makeAbsent()
|
||||||
|
},
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val HEADER_ROW = 0
|
private const val HEADER_ROW = 0
|
||||||
|
|
@ -210,15 +255,17 @@ fun SeriesDetailsContent(
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
onClickPerson: (Person) -> Unit,
|
onClickPerson: (Person) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (Int, BaseItem) -> Unit,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
playOnClick: () -> Unit,
|
playOnClick: () -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
favoriteOnClick: () -> Unit,
|
favoriteOnClick: () -> Unit,
|
||||||
|
moreActions: MoreDialogActions,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
|
||||||
|
|
@ -227,6 +274,7 @@ fun SeriesDetailsContent(
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
}
|
}
|
||||||
|
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -349,11 +397,14 @@ fun SeriesDetailsContent(
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(R.string.tv_seasons),
|
title = stringResource(R.string.tv_seasons),
|
||||||
items = seasons,
|
items = seasons,
|
||||||
onClickItem = {
|
onClickItem = { index, item ->
|
||||||
position = SEASONS_ROW
|
position = SEASONS_ROW
|
||||||
onClickItem.invoke(it)
|
onClickItem.invoke(index, item)
|
||||||
|
},
|
||||||
|
onLongClickItem = { index, item ->
|
||||||
|
position = SEASONS_ROW
|
||||||
|
onLongClickItem.invoke(index, item)
|
||||||
},
|
},
|
||||||
onLongClickItem = onLongClickItem,
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -379,7 +430,21 @@ fun SeriesDetailsContent(
|
||||||
position = PEOPLE_ROW
|
position = PEOPLE_ROW
|
||||||
onClickPerson.invoke(it)
|
onClickPerson.invoke(it)
|
||||||
},
|
},
|
||||||
onLongClick = {},
|
onLongClick = { index, person ->
|
||||||
|
position = PEOPLE_ROW
|
||||||
|
val items =
|
||||||
|
buildMoreDialogItemsForPerson(
|
||||||
|
context = context,
|
||||||
|
person = person,
|
||||||
|
actions = moreActions,
|
||||||
|
)
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = true,
|
||||||
|
title = person.name ?: "",
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -392,11 +457,29 @@ fun SeriesDetailsContent(
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(R.string.more_like_this),
|
title = stringResource(R.string.more_like_this),
|
||||||
items = similar,
|
items = similar,
|
||||||
onClickItem = {
|
onClickItem = { index, item ->
|
||||||
position = SIMILAR_ROW
|
position = SIMILAR_ROW
|
||||||
onClickItem.invoke(it)
|
onClickItem.invoke(index, item)
|
||||||
|
},
|
||||||
|
onLongClickItem = { index, item ->
|
||||||
|
position = SIMILAR_ROW
|
||||||
|
val items =
|
||||||
|
buildMoreDialogItemsForHome(
|
||||||
|
context = context,
|
||||||
|
item = item,
|
||||||
|
seriesId = null,
|
||||||
|
playbackPosition = item.playbackPosition,
|
||||||
|
watched = item.played,
|
||||||
|
favorite = item.favorite,
|
||||||
|
actions = moreActions,
|
||||||
|
)
|
||||||
|
moreDialog =
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = true,
|
||||||
|
title = item.name ?: "",
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onLongClickItem = {},
|
|
||||||
cardContent = { index, item, mod, onClick, onLongClick ->
|
cardContent = { index, item, mod, onClick, onLongClick ->
|
||||||
SeasonCard(
|
SeasonCard(
|
||||||
item = item,
|
item = item,
|
||||||
|
|
@ -418,6 +501,16 @@ fun SeriesDetailsContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
moreDialog?.let { params ->
|
||||||
|
DialogPopup(
|
||||||
|
showDialog = true,
|
||||||
|
title = params.title,
|
||||||
|
dialogItems = params.items,
|
||||||
|
onDismissRequest = { moreDialog = null },
|
||||||
|
dismissOnClick = true,
|
||||||
|
waitToLoad = params.fromLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import com.github.damontecres.wholphin.ui.components.chooseVersionParams
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||||
|
|
@ -196,29 +197,28 @@ fun SeriesOverview(
|
||||||
favorite = ep.data.userData?.isFavorite ?: false,
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
series = series,
|
series = series,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.sourceId,
|
||||||
|
actions =
|
||||||
|
MoreDialogActions(
|
||||||
navigateTo = viewModel::navigateTo,
|
navigateTo = viewModel::navigateTo,
|
||||||
onClickWatch = { played ->
|
onClickWatch = { itemId, watched ->
|
||||||
episodeList
|
|
||||||
?.getOrNull(position.episodeRowIndex)
|
|
||||||
?.let {
|
|
||||||
viewModel.setWatched(
|
viewModel.setWatched(
|
||||||
it.id,
|
itemId,
|
||||||
played,
|
watched,
|
||||||
position.episodeRowIndex,
|
position.episodeRowIndex,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onClickFavorite = { favorite ->
|
onClickFavorite = { itemId, favorite ->
|
||||||
episodeList
|
|
||||||
?.getOrNull(position.episodeRowIndex)
|
|
||||||
?.let {
|
|
||||||
viewModel.setFavorite(
|
viewModel.setFavorite(
|
||||||
it.id,
|
itemId,
|
||||||
favorite,
|
favorite,
|
||||||
position.episodeRowIndex,
|
position.episodeRowIndex,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
onClickAddPlaylist = {
|
||||||
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
|
showPlaylistDialog = it
|
||||||
|
},
|
||||||
|
),
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseVersionParams(
|
chooseVersionParams(
|
||||||
|
|
@ -254,10 +254,6 @@ fun SeriesOverview(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickAddPlaylist = {
|
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
|
||||||
showPlaylistDialog = ep.id
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -350,7 +346,7 @@ fun SeriesOverview(
|
||||||
ItemDetailsDialog(
|
ItemDetailsDialog(
|
||||||
info = info,
|
info = info,
|
||||||
showFilePath =
|
showFilePath =
|
||||||
viewModel.serverRepository.currentUserDto
|
viewModel.serverRepository.currentUserDto.value
|
||||||
?.policy
|
?.policy
|
||||||
?.isAdministrator == true,
|
?.isAdministrator == true,
|
||||||
onDismissRequest = { overviewDialog = null },
|
onDismissRequest = { overviewDialog = null },
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
|
@ -50,6 +51,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.TabRow
|
import com.github.damontecres.wholphin.ui.components.TabRow
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.formatDateTime
|
import com.github.damontecres.wholphin.util.formatDateTime
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -77,6 +79,9 @@ fun SeriesOverviewContent(
|
||||||
) {
|
) {
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
var selectedTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) }
|
var selectedTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) }
|
||||||
|
LaunchedEffect(selectedTabIndex) {
|
||||||
|
logTab("series_overview", selectedTabIndex)
|
||||||
|
}
|
||||||
val tabRowFocusRequester = remember { FocusRequester() }
|
val tabRowFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val focusedEpisode =
|
val focusedEpisode =
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
|
@ -36,9 +37,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
|
@ -61,6 +60,7 @@ class SeriesViewModel
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
private val itemPlaybackRepository: ItemPlaybackRepository,
|
private val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
private val themeSongPlayer: ThemeSongPlayer,
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
private lateinit var seriesId: UUID
|
private lateinit var seriesId: UUID
|
||||||
private lateinit var prefs: UserPreferences
|
private lateinit var prefs: UserPreferences
|
||||||
|
|
@ -121,7 +121,7 @@ class SeriesViewModel
|
||||||
api.libraryApi
|
api.libraryApi
|
||||||
.getSimilarItems(
|
.getSimilarItems(
|
||||||
GetSimilarItemsRequest(
|
GetSimilarItemsRequest(
|
||||||
userId = serverRepository.currentUser?.id,
|
userId = serverRepository.currentUser.value?.id,
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
fields = SlimItemFields,
|
fields = SlimItemFields,
|
||||||
limit = 25,
|
limit = 25,
|
||||||
|
|
@ -240,11 +240,7 @@ class SeriesViewModel
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
listIndex: Int?,
|
listIndex: Int?,
|
||||||
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
if (played) {
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
api.playStateApi.markPlayedItem(itemId)
|
|
||||||
} else {
|
|
||||||
api.playStateApi.markUnplayedItem(itemId)
|
|
||||||
}
|
|
||||||
listIndex?.let {
|
listIndex?.let {
|
||||||
refreshEpisode(itemId, listIndex)
|
refreshEpisode(itemId, listIndex)
|
||||||
}
|
}
|
||||||
|
|
@ -255,11 +251,7 @@ class SeriesViewModel
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
listIndex: Int?,
|
listIndex: Int?,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
if (favorite) {
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
api.userLibraryApi.markFavoriteItem(itemId)
|
|
||||||
} else {
|
|
||||||
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
|
||||||
}
|
|
||||||
if (listIndex != null) {
|
if (listIndex != null) {
|
||||||
refreshEpisode(itemId, listIndex)
|
refreshEpisode(itemId, listIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -279,11 +271,7 @@ class SeriesViewModel
|
||||||
|
|
||||||
fun setWatchedSeries(played: Boolean) =
|
fun setWatchedSeries(played: Boolean) =
|
||||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
if (played) {
|
favoriteWatchManager.setWatched(seriesId, played)
|
||||||
api.playStateApi.markPlayedItem(seriesId)
|
|
||||||
} else {
|
|
||||||
api.playStateApi.markUnplayedItem(seriesId)
|
|
||||||
}
|
|
||||||
val series = fetchItem(seriesId)
|
val series = fetchItem(seriesId)
|
||||||
val seasons = getSeasons(series)
|
val seasons = getSeasons(series)
|
||||||
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
|
@ -75,7 +76,6 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage(
|
fun HomePage(
|
||||||
|
|
@ -121,22 +121,20 @@ fun HomePage(
|
||||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
HomePageContent(
|
HomePageContent(
|
||||||
watchingRows + latestRows,
|
watchingRows + latestRows,
|
||||||
onClickItem = {
|
onClickItem = { position, item ->
|
||||||
viewModel.navigationManager.navigateTo(it.destination())
|
viewModel.navigationManager.navigateTo(item.destination())
|
||||||
},
|
},
|
||||||
onLongClickItem = {
|
onLongClickItem = { position, item ->
|
||||||
val dialogItems =
|
val dialogItems =
|
||||||
buildMoreDialogItemsForHome(
|
buildMoreDialogItemsForHome(
|
||||||
context = context,
|
context = context,
|
||||||
item = it,
|
item = item,
|
||||||
seriesId = it.data.seriesId,
|
seriesId = item.data.seriesId,
|
||||||
playbackPosition =
|
playbackPosition = item.playbackPosition,
|
||||||
it.data.userData
|
watched = item.played,
|
||||||
?.playbackPositionTicks
|
favorite = item.favorite,
|
||||||
?.ticks
|
actions =
|
||||||
?: Duration.ZERO,
|
MoreDialogActions(
|
||||||
watched = it.data.userData?.played ?: false,
|
|
||||||
favorite = it.data.userData?.isFavorite ?: false,
|
|
||||||
navigateTo = viewModel.navigationManager::navigateTo,
|
navigateTo = viewModel.navigationManager::navigateTo,
|
||||||
onClickWatch = { itemId, played ->
|
onClickWatch = { itemId, played ->
|
||||||
viewModel.setWatched(itemId, played)
|
viewModel.setWatched(itemId, played)
|
||||||
|
|
@ -148,10 +146,11 @@ fun HomePage(
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
showPlaylistDialog = itemId
|
showPlaylistDialog = itemId
|
||||||
},
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
dialog =
|
dialog =
|
||||||
DialogParams(
|
DialogParams(
|
||||||
title = it.title ?: "",
|
title = item.title ?: "",
|
||||||
fromLongClick = true,
|
fromLongClick = true,
|
||||||
items = dialogItems,
|
items = dialogItems,
|
||||||
)
|
)
|
||||||
|
|
@ -190,8 +189,8 @@ fun HomePage(
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePageContent(
|
fun HomePageContent(
|
||||||
homeRows: List<HomeRowLoadingState>,
|
homeRows: List<HomeRowLoadingState>,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (RowColumn, BaseItem) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (RowColumn, BaseItem) -> Unit,
|
||||||
showClock: Boolean,
|
showClock: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
||||||
|
|
@ -317,14 +316,18 @@ fun HomePageContent(
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = row.title,
|
title = row.title,
|
||||||
items = row.items,
|
items = row.items,
|
||||||
onClickItem = onClickItem,
|
onClickItem = { index, item ->
|
||||||
|
onClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||||
|
},
|
||||||
cardOnFocus = { isFocused, index ->
|
cardOnFocus = { isFocused, index ->
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
focusedItem = row.items.getOrNull(index)
|
focusedItem = row.items.getOrNull(index)
|
||||||
position = RowColumn(rowIndex, index)
|
position = RowColumn(rowIndex, index)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = { index, item ->
|
||||||
|
onLongClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||||
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -339,6 +342,7 @@ fun HomePageContent(
|
||||||
item?.data?.indexNumber?.let { "E$it" }
|
item?.data?.indexNumber?.let { "E$it" }
|
||||||
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
|
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
|
||||||
played = item?.data?.userData?.played ?: false,
|
played = item?.data?.userData?.played ?: false,
|
||||||
|
favorite = item?.favorite ?: false,
|
||||||
playPercent =
|
playPercent =
|
||||||
item?.data?.userData?.playedPercentage
|
item?.data?.userData?.playedPercentage
|
||||||
?: 0.0,
|
?: 0.0,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -27,7 +28,6 @@ import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||||
|
|
@ -50,6 +50,7 @@ class HomeViewModel
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
@ -76,7 +77,7 @@ class HomeViewModel
|
||||||
) {
|
) {
|
||||||
Timber.d("init HomeViewModel")
|
Timber.d("init HomeViewModel")
|
||||||
|
|
||||||
serverRepository.currentUserDto?.let { userDto ->
|
serverRepository.currentUserDto.value?.let { userDto ->
|
||||||
val includedIds =
|
val includedIds =
|
||||||
navDrawerItemRepository
|
navDrawerItemRepository
|
||||||
.getFilteredNavDrawerItems(navDrawerItemRepository.getNavDrawerItems())
|
.getFilteredNavDrawerItems(navDrawerItemRepository.getNavDrawerItems())
|
||||||
|
|
@ -258,11 +259,7 @@ class HomeViewModel
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
if (played) {
|
favoriteWatchManager.setWatched(itemId, played)
|
||||||
api.playStateApi.markPlayedItem(itemId)
|
|
||||||
} else {
|
|
||||||
api.playStateApi.markUnplayedItem(itemId)
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
init(preferences)
|
init(preferences)
|
||||||
}
|
}
|
||||||
|
|
@ -272,11 +269,7 @@ class HomeViewModel
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
if (favorite) {
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
api.userLibraryApi.markFavoriteItem(itemId)
|
|
||||||
} else {
|
|
||||||
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
init(preferences)
|
init(preferences)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,9 @@ fun SearchPage(
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.tryRequestFocus()
|
focusRequester.tryRequestFocus()
|
||||||
}
|
}
|
||||||
|
val onClickItem = { index: Int, item: BaseItem ->
|
||||||
|
viewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 44.dp),
|
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 44.dp),
|
||||||
|
|
@ -207,7 +210,7 @@ fun SearchPage(
|
||||||
rowIndex = MOVIE_ROW,
|
rowIndex = MOVIE_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
|
onClickItem = onClickItem,
|
||||||
onClickPosition = { position = it },
|
onClickPosition = { position = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
@ -217,7 +220,7 @@ fun SearchPage(
|
||||||
rowIndex = COLLECTION_ROW,
|
rowIndex = COLLECTION_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
|
onClickItem = onClickItem,
|
||||||
onClickPosition = { position = it },
|
onClickPosition = { position = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
@ -227,7 +230,7 @@ fun SearchPage(
|
||||||
rowIndex = SERIES_ROW,
|
rowIndex = SERIES_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
|
onClickItem = onClickItem,
|
||||||
onClickPosition = { position = it },
|
onClickPosition = { position = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
@ -237,7 +240,7 @@ fun SearchPage(
|
||||||
rowIndex = EPISODE_ROW,
|
rowIndex = EPISODE_ROW,
|
||||||
position = position,
|
position = position,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
|
onClickItem = onClickItem,
|
||||||
onClickPosition = { position = it },
|
onClickPosition = { position = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
|
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
|
||||||
|
|
@ -268,7 +271,7 @@ fun LazyListScope.searchResultRow(
|
||||||
rowIndex: Int,
|
rowIndex: Int,
|
||||||
position: RowColumn,
|
position: RowColumn,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (Int, BaseItem) -> Unit,
|
||||||
onClickPosition: (RowColumn) -> Unit,
|
onClickPosition: (RowColumn) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
cardContent: @Composable (
|
cardContent: @Composable (
|
||||||
|
|
@ -328,7 +331,7 @@ fun LazyListScope.searchResultRow(
|
||||||
title = title,
|
title = title,
|
||||||
items = r.items,
|
items = r.items,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = {},
|
onLongClickItem = { _, _ -> },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
cardContent = cardContent,
|
cardContent = cardContent,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import androidx.navigation3.ui.NavDisplay
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is generally the root composable of the of the app
|
* This is generally the root composable of the of the app
|
||||||
|
|
@ -38,7 +38,6 @@ fun ApplicationContent(
|
||||||
entryProvider = { key ->
|
entryProvider = { key ->
|
||||||
key as Destination
|
key as Destination
|
||||||
val contentKey = "${key}_${server?.id}_${user?.id}"
|
val contentKey = "${key}_${server?.id}_${user?.id}"
|
||||||
Timber.d("Navigate: %s", key)
|
|
||||||
NavEntry(key, contentKey = contentKey) {
|
NavEntry(key, contentKey = contentKey) {
|
||||||
if (key.fullScreen) {
|
if (key.fullScreen) {
|
||||||
DestinationContent(
|
DestinationContent(
|
||||||
|
|
@ -47,7 +46,7 @@ fun ApplicationContent(
|
||||||
deviceProfile = deviceProfile,
|
deviceProfile = deviceProfile,
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
} else {
|
} else if (user != null && server != null) {
|
||||||
NavDrawer(
|
NavDrawer(
|
||||||
destination = key,
|
destination = key,
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
@ -56,6 +55,8 @@ fun ApplicationContent(
|
||||||
server = server,
|
server = server,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
ErrorMessage("Trying to go to $key without a user logged in", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.navigation3.runtime.NavKey
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||||
|
|
@ -29,7 +30,9 @@ sealed class Destination(
|
||||||
data object ServerList : Destination(true)
|
data object ServerList : Destination(true)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object UserList : Destination(true)
|
data class UserList(
|
||||||
|
val server: JellyfinServer,
|
||||||
|
) : Destination(true)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Home(
|
data class Home(
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ fun DestinationContent(
|
||||||
)
|
)
|
||||||
|
|
||||||
Destination.ServerList -> SwitchServerContent(modifier)
|
Destination.ServerList -> SwitchServerContent(modifier)
|
||||||
Destination.UserList -> SwitchUserContent(modifier)
|
is Destination.UserList -> SwitchUserContent(destination.server, modifier)
|
||||||
|
|
||||||
is Destination.Settings ->
|
is Destination.Settings ->
|
||||||
PreferencesPage(
|
PreferencesPage(
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import timber.log.Timber
|
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -141,7 +140,7 @@ class NavDrawerViewModel
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timber.v("Found $index => $key")
|
// Timber.v("Found $index => $key")
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
selectedIndex.setValueOnMain(index)
|
selectedIndex.setValueOnMain(index)
|
||||||
break
|
break
|
||||||
|
|
@ -198,8 +197,8 @@ data class ServerNavDrawerItem(
|
||||||
fun NavDrawer(
|
fun NavDrawer(
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
user: JellyfinUser?,
|
user: JellyfinUser,
|
||||||
server: JellyfinServer?,
|
server: JellyfinServer,
|
||||||
deviceProfile: DeviceProfile,
|
deviceProfile: DeviceProfile,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: NavDrawerViewModel =
|
viewModel: NavDrawerViewModel =
|
||||||
|
|
@ -319,7 +318,11 @@ fun NavDrawer(
|
||||||
selected = false,
|
selected = false,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.navigationManager.navigateTo(Destination.UserList)
|
viewModel.navigationManager.navigateToFromDrawer(
|
||||||
|
Destination.UserList(
|
||||||
|
server,
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.github.damontecres.wholphin.ui.nav
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import org.acra.ACRA
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -18,6 +20,7 @@ class NavigationManager
|
||||||
*/
|
*/
|
||||||
fun navigateTo(destination: Destination) {
|
fun navigateTo(destination: Destination) {
|
||||||
backStack.add(destination)
|
backStack.add(destination)
|
||||||
|
log()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,7 +28,13 @@ class NavigationManager
|
||||||
*/
|
*/
|
||||||
fun navigateToFromDrawer(destination: Destination) {
|
fun navigateToFromDrawer(destination: Destination) {
|
||||||
goToHome()
|
goToHome()
|
||||||
|
if (destination == Destination.ServerList || destination is Destination.UserList) {
|
||||||
backStack.add(destination)
|
backStack.add(destination)
|
||||||
|
(0..<backStack.size - 1).forEach { _ -> backStack.removeAt(0) }
|
||||||
|
} else {
|
||||||
|
backStack.add(destination)
|
||||||
|
}
|
||||||
|
log()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,6 +42,7 @@ class NavigationManager
|
||||||
*/
|
*/
|
||||||
fun goBack() {
|
fun goBack() {
|
||||||
backStack.removeLastOrNull()
|
backStack.removeLastOrNull()
|
||||||
|
log()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,6 +55,7 @@ class NavigationManager
|
||||||
if (backStack[0] !is Destination.Home) {
|
if (backStack[0] !is Destination.Home) {
|
||||||
backStack[0] = Destination.Home()
|
backStack[0] = Destination.Home()
|
||||||
}
|
}
|
||||||
|
log()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,5 +65,12 @@ class NavigationManager
|
||||||
goToHome()
|
goToHome()
|
||||||
val id = (backStack[0] as Destination.Home).id + 1
|
val id = (backStack[0] as Destination.Home).id + 1
|
||||||
backStack[0] = Destination.Home(id)
|
backStack[0] = Destination.Home(id)
|
||||||
|
log()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun log() {
|
||||||
|
val dest = backStack.lastOrNull().toString()
|
||||||
|
Timber.i("Current Destination: %s", dest)
|
||||||
|
ACRA.errorReporter.putCustomData("destination", dest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,7 @@ class PlaybackViewModel
|
||||||
if (itemPlayback != null) {
|
if (itemPlayback != null) {
|
||||||
itemPlayback
|
itemPlayback
|
||||||
} else {
|
} else {
|
||||||
val user = serverRepository.currentUser!!
|
serverRepository.currentUser.value?.let { user ->
|
||||||
itemPlaybackDao.getItem(user, base.id)?.let {
|
itemPlaybackDao.getItem(user, base.id)?.let {
|
||||||
Timber.v("Fetched itemPlayback from DB: %s", it)
|
Timber.v("Fetched itemPlayback from DB: %s", it)
|
||||||
if (it.sourceId != null) {
|
if (it.sourceId != null) {
|
||||||
|
|
@ -282,6 +282,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val mediaSource = chooseSource(base, playbackConfig)
|
val mediaSource = chooseSource(base, playbackConfig)
|
||||||
|
|
||||||
if (mediaSource == null) {
|
if (mediaSource == null) {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class PreferencesViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
serverRepository.currentUser?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems()
|
allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems()
|
||||||
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
|
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
|
||||||
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
|
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
|
||||||
|
|
@ -60,7 +60,7 @@ class PreferencesViewModel
|
||||||
|
|
||||||
fun updatePins(newSelectedItems: List<NavDrawerItem>) {
|
fun updatePins(newSelectedItems: List<NavDrawerItem>) {
|
||||||
viewModelScope.launchIO(ExceptionHandler(true)) {
|
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||||
serverRepository.currentUser?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
val disabledItems =
|
val disabledItems =
|
||||||
mutableListOf<NavDrawerItem>().apply {
|
mutableListOf<NavDrawerItem>().apply {
|
||||||
addAll(allNavDrawerItems)
|
addAll(allNavDrawerItems)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ fun ServerList(
|
||||||
servers: List<JellyfinServer>,
|
servers: List<JellyfinServer>,
|
||||||
connectionStatus: Map<UUID, ServerConnectionStatus>,
|
connectionStatus: Map<UUID, ServerConnectionStatus>,
|
||||||
onSwitchServer: (JellyfinServer) -> Unit,
|
onSwitchServer: (JellyfinServer) -> Unit,
|
||||||
|
onTestServer: (JellyfinServer) -> Unit,
|
||||||
onAddServer: () -> Unit,
|
onAddServer: () -> Unit,
|
||||||
onRemoveServer: (JellyfinServer) -> Unit,
|
onRemoveServer: (JellyfinServer) -> Unit,
|
||||||
allowAdd: Boolean,
|
allowAdd: Boolean,
|
||||||
|
|
@ -61,7 +62,7 @@ fun ServerList(
|
||||||
items(servers) { server ->
|
items(servers) { server ->
|
||||||
val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending
|
val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending
|
||||||
ListItem(
|
ListItem(
|
||||||
enabled = status == ServerConnectionStatus.Success,
|
enabled = true,
|
||||||
selected = false,
|
selected = false,
|
||||||
headlineContent = { Text(text = server.name?.ifBlank { null } ?: server.url) },
|
headlineContent = { Text(text = server.name?.ifBlank { null } ?: server.url) },
|
||||||
supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) },
|
supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) },
|
||||||
|
|
@ -77,12 +78,18 @@ fun ServerList(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Warning,
|
imageVector = Icons.Default.Warning,
|
||||||
contentDescription = status.message,
|
contentDescription = status.message,
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = MaterialTheme.colorScheme.errorContainer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = { onSwitchServer.invoke(server) },
|
onClick = {
|
||||||
|
when (status) {
|
||||||
|
ServerConnectionStatus.Success -> onSwitchServer.invoke(server)
|
||||||
|
ServerConnectionStatus.Pending -> {}
|
||||||
|
is ServerConnectionStatus.Error -> onTestServer.invoke(server)
|
||||||
|
}
|
||||||
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
if (allowDelete) {
|
if (allowDelete) {
|
||||||
showDeleteDialog = server
|
showDeleteDialog = server
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,8 @@ import com.github.damontecres.wholphin.util.LoadingState
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchServerContent(
|
fun SwitchServerContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SwitchUserViewModel = hiltViewModel(),
|
viewModel: SwitchServerViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val currentServer = viewModel.serverRepository.currentServer
|
|
||||||
val servers by viewModel.servers.observeAsState(listOf())
|
val servers by viewModel.servers.observeAsState(listOf())
|
||||||
val serverStatus by viewModel.serverStatus.observeAsState(mapOf())
|
val serverStatus by viewModel.serverStatus.observeAsState(mapOf())
|
||||||
|
|
||||||
|
|
@ -90,6 +89,9 @@ fun SwitchServerContent(
|
||||||
onSwitchServer = {
|
onSwitchServer = {
|
||||||
viewModel.addServer(it.url)
|
viewModel.addServer(it.url)
|
||||||
},
|
},
|
||||||
|
onTestServer = {
|
||||||
|
viewModel.testServer(it)
|
||||||
|
},
|
||||||
onAddServer = {
|
onAddServer = {
|
||||||
showAddServer = true
|
showAddServer = true
|
||||||
},
|
},
|
||||||
|
|
@ -142,6 +144,9 @@ fun SwitchServerContent(
|
||||||
onSwitchServer = {
|
onSwitchServer = {
|
||||||
viewModel.addServer(it.url)
|
viewModel.addServer(it.url)
|
||||||
},
|
},
|
||||||
|
onTestServer = {
|
||||||
|
viewModel.testServer(it)
|
||||||
|
},
|
||||||
onAddServer = {},
|
onAddServer = {},
|
||||||
onRemoveServer = {},
|
onRemoveServer = {},
|
||||||
allowAdd = false,
|
allowAdd = false,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.setup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
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.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.Jellyfin
|
||||||
|
import org.jellyfin.sdk.api.client.HttpClientOptions
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SwitchServerViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
val jellyfin: Jellyfin,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
val serverDao: JellyfinServerDao,
|
||||||
|
val navigationManager: NavigationManager,
|
||||||
|
) : ViewModel() {
|
||||||
|
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
|
||||||
|
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
|
||||||
|
val serverQuickConnect = MutableLiveData<Map<UUID, Boolean>>(mapOf())
|
||||||
|
|
||||||
|
val discoveredServers = MutableLiveData<List<JellyfinServer>>(listOf())
|
||||||
|
|
||||||
|
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
|
fun clearAddServerState() {
|
||||||
|
addServerState.value = LoadingState.Pending
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
serverStatus.value = mapOf()
|
||||||
|
serverQuickConnect.value = mapOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
val allServers =
|
||||||
|
serverDao
|
||||||
|
.getServers()
|
||||||
|
.map { it.server }
|
||||||
|
.sortedWith(compareBy<JellyfinServer> { it.name }.thenBy { it.url })
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
servers.value = allServers
|
||||||
|
}
|
||||||
|
allServers.forEach { server ->
|
||||||
|
internalTestServer(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testServer(server: JellyfinServer) {
|
||||||
|
serverStatus.value =
|
||||||
|
serverStatus.value!!.toMutableMap().apply {
|
||||||
|
put(
|
||||||
|
server.id,
|
||||||
|
ServerConnectionStatus.Pending,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
delay(1000)
|
||||||
|
val result = internalTestServer(server)
|
||||||
|
if (result == ServerConnectionStatus.Success) {
|
||||||
|
showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT)
|
||||||
|
} else if (result is ServerConnectionStatus.Error) {
|
||||||
|
showToast(context, result.message ?: "Error", Toast.LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun internalTestServer(server: JellyfinServer): ServerConnectionStatus =
|
||||||
|
try {
|
||||||
|
jellyfin
|
||||||
|
.createApi(
|
||||||
|
server.url,
|
||||||
|
httpClientOptions =
|
||||||
|
HttpClientOptions(
|
||||||
|
requestTimeout = 6.seconds,
|
||||||
|
connectTimeout = 6.seconds,
|
||||||
|
socketTimeout = 6.seconds,
|
||||||
|
),
|
||||||
|
).systemApi
|
||||||
|
.getPublicSystemInfo()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
serverStatus.value =
|
||||||
|
serverStatus.value!!.toMutableMap().apply {
|
||||||
|
put(
|
||||||
|
server.id,
|
||||||
|
ServerConnectionStatus.Success,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerConnectionStatus.Success
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
val status = ServerConnectionStatus.Error(ex.localizedMessage)
|
||||||
|
Timber.w(ex, "Error checking server ${server.url}")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
serverStatus.value =
|
||||||
|
serverStatus.value!!.toMutableMap().apply {
|
||||||
|
put(
|
||||||
|
server.id,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addServer(serverUrl: String) {
|
||||||
|
addServerState.value = LoadingState.Loading
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
try {
|
||||||
|
val serverInfo by jellyfin
|
||||||
|
.createApi(serverUrl)
|
||||||
|
.systemApi
|
||||||
|
.getPublicSystemInfo()
|
||||||
|
val id = serverInfo.id?.toUUIDOrNull()
|
||||||
|
if (id != null && serverInfo.startupWizardCompleted == true) {
|
||||||
|
val server =
|
||||||
|
JellyfinServer(
|
||||||
|
id = id,
|
||||||
|
name = serverInfo.serverName,
|
||||||
|
url = serverUrl,
|
||||||
|
)
|
||||||
|
serverRepository.addAndChangeServer(server)
|
||||||
|
val quickConnect =
|
||||||
|
jellyfin
|
||||||
|
.createApi(serverUrl)
|
||||||
|
.quickConnectApi
|
||||||
|
.getQuickConnectEnabled()
|
||||||
|
.content
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
serverQuickConnect.value =
|
||||||
|
serverQuickConnect.value!!.toMutableMap().apply {
|
||||||
|
put(id, quickConnect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
addServerState.value = LoadingState.Success
|
||||||
|
navigationManager.navigateTo(Destination.UserList(server))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
addServerState.value =
|
||||||
|
LoadingState.Error("Server returned invalid response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Error creating API for $serverUrl")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
addServerState.value =
|
||||||
|
LoadingState.Error(exception = ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeServer(server: JellyfinServer) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
serverRepository.removeServer(server)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun discoverServers() {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
jellyfin.discovery.discoverLocalServers().collect { server ->
|
||||||
|
val newServerList =
|
||||||
|
discoveredServers.value!!
|
||||||
|
.toMutableList()
|
||||||
|
.apply {
|
||||||
|
add(
|
||||||
|
JellyfinServer(
|
||||||
|
server.id.toUUID(),
|
||||||
|
server.name,
|
||||||
|
server.address,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
discoveredServers.value = newServerList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,7 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||||
|
|
@ -49,17 +50,20 @@ import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchUserContent(
|
fun SwitchUserContent(
|
||||||
|
currentServer: JellyfinServer,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SwitchUserViewModel = hiltViewModel(),
|
viewModel: SwitchUserViewModel =
|
||||||
|
hiltViewModel<SwitchUserViewModel, SwitchUserViewModel.Factory>(
|
||||||
|
creationCallback = { it.create(currentServer) },
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val currentServer = viewModel.serverRepository.currentServer
|
// val currentServer by viewModel.serverRepository.currentServer.observeAsState()
|
||||||
val currentUser = viewModel.serverRepository.currentUser
|
val currentUser by viewModel.serverRepository.currentUser.observeAsState()
|
||||||
val users by viewModel.users.observeAsState(listOf())
|
val users by viewModel.users.observeAsState(listOf())
|
||||||
|
|
||||||
val serverQuickConnect by viewModel.serverQuickConnect.observeAsState(mapOf())
|
val quickConnectEnabled by viewModel.serverQuickConnect.observeAsState(false)
|
||||||
val quickConnectEnabled = currentServer?.let { serverQuickConnect[it.id] ?: false } ?: false
|
|
||||||
val quickConnect by viewModel.quickConnectState.observeAsState(null)
|
val quickConnect by viewModel.quickConnectState.observeAsState(null)
|
||||||
var showAddUser by remember { mutableStateOf(false) }
|
var showAddUser by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -109,7 +113,7 @@ fun SwitchUserContent(
|
||||||
users = users,
|
users = users,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
onSwitchUser = { user ->
|
onSwitchUser = { user ->
|
||||||
viewModel.switchUser(server, user)
|
viewModel.switchUser(user)
|
||||||
},
|
},
|
||||||
onAddUser = { showAddUser = true },
|
onAddUser = { showAddUser = true },
|
||||||
onRemoveUser = { user ->
|
onRemoveUser = { user ->
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,18 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.Jellyfin
|
import org.jellyfin.sdk.Jellyfin
|
||||||
import org.jellyfin.sdk.api.client.HttpClientOptions
|
import org.jellyfin.sdk.api.client.HttpClientOptions
|
||||||
|
|
@ -25,42 +30,41 @@ import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
import org.jellyfin.sdk.model.api.QuickConnectDto
|
import org.jellyfin.sdk.model.api.QuickConnectDto
|
||||||
import org.jellyfin.sdk.model.api.QuickConnectResult
|
import org.jellyfin.sdk.model.api.QuickConnectResult
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel(assistedFactory = SwitchUserViewModel.Factory::class)
|
||||||
class SwitchUserViewModel
|
class SwitchUserViewModel
|
||||||
@Inject
|
@AssistedInject
|
||||||
constructor(
|
constructor(
|
||||||
val jellyfin: Jellyfin,
|
val jellyfin: Jellyfin,
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val serverDao: JellyfinServerDao,
|
val serverDao: JellyfinServerDao,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
|
@Assisted val server: JellyfinServer,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
|
@AssistedFactory
|
||||||
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
|
interface Factory {
|
||||||
val serverQuickConnect = MutableLiveData<Map<UUID, Boolean>>(mapOf())
|
fun create(server: JellyfinServer): SwitchUserViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) {
|
||||||
|
serverRepository.switchServerOrUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverQuickConnect = MutableLiveData<Boolean>(false)
|
||||||
|
|
||||||
val users = MutableLiveData<List<JellyfinUser>>(listOf())
|
val users = MutableLiveData<List<JellyfinUser>>(listOf())
|
||||||
val quickConnectState = MutableLiveData<QuickConnectResult?>(null)
|
val quickConnectState = MutableLiveData<QuickConnectResult?>(null)
|
||||||
|
|
||||||
private var quickConnectJob: Job? = null
|
private var quickConnectJob: Job? = null
|
||||||
|
|
||||||
val discoveredServers = MutableLiveData<List<JellyfinServer>>(listOf())
|
|
||||||
|
|
||||||
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
|
||||||
val switchUserState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val switchUserState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
val loginAttempts = MutableLiveData(0)
|
val loginAttempts = MutableLiveData(0)
|
||||||
|
|
||||||
fun clearAddServerState() {
|
|
||||||
addServerState.value = LoadingState.Pending
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearSwitchUserState() {
|
fun clearSwitchUserState() {
|
||||||
switchUserState.value = LoadingState.Pending
|
switchUserState.value = LoadingState.Pending
|
||||||
}
|
}
|
||||||
|
|
@ -74,23 +78,17 @@ class SwitchUserViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
viewModelScope.launchIO {
|
|
||||||
quickConnectJob?.cancel()
|
quickConnectJob?.cancel()
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
users.setValueOnMain(listOf())
|
||||||
|
val serverUsers =
|
||||||
|
serverDao.getServer(server.id)?.users?.sortedBy { it.name } ?: listOf()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
users.value = listOf()
|
users.setValueOnMain(serverUsers)
|
||||||
serverStatus.value = mapOf()
|
}
|
||||||
serverQuickConnect.value = mapOf()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val allServers =
|
viewModelScope.launchIO {
|
||||||
serverDao
|
|
||||||
.getServers()
|
|
||||||
.map { it.server }
|
|
||||||
.sortedWith(compareBy<JellyfinServer> { it.name }.thenBy { it.url })
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
servers.value = allServers
|
|
||||||
}
|
|
||||||
allServers.forEach { server ->
|
|
||||||
try {
|
try {
|
||||||
jellyfin
|
jellyfin
|
||||||
.createApi(
|
.createApi(
|
||||||
|
|
@ -109,56 +107,18 @@ class SwitchUserViewModel
|
||||||
.quickConnectApi
|
.quickConnectApi
|
||||||
.getQuickConnectEnabled()
|
.getQuickConnectEnabled()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
serverStatus.value =
|
serverQuickConnect.value = quickConnect
|
||||||
serverStatus.value!!.toMutableMap().apply {
|
|
||||||
put(
|
|
||||||
server.id,
|
|
||||||
ServerConnectionStatus.Success,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
serverQuickConnect.value =
|
|
||||||
serverQuickConnect.value!!.toMutableMap().apply {
|
|
||||||
put(server.id, quickConnect)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.w(ex, "Error checking quick connect for server ${server.url}")
|
Timber.w(ex, "Error checking quick connect for server ${server.url}")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
serverStatus.value =
|
serverQuickConnect.value = false
|
||||||
serverStatus.value!!.toMutableMap().apply {
|
|
||||||
put(
|
|
||||||
server.id,
|
|
||||||
ServerConnectionStatus.Error(ex.localizedMessage),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
serverRepository.currentServer?.let {
|
|
||||||
val quickConnect =
|
|
||||||
jellyfin
|
|
||||||
.createApi(it.url)
|
|
||||||
.quickConnectApi
|
|
||||||
.getQuickConnectEnabled()
|
|
||||||
.content
|
|
||||||
val serverUsers = serverDao.getServer(it.id)?.users?.sortedBy { it.name } ?: listOf()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
serverQuickConnect.value =
|
|
||||||
serverQuickConnect.value!!.toMutableMap().apply {
|
|
||||||
put(it.id, quickConnect)
|
|
||||||
}
|
|
||||||
users.value = serverUsers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchUser(
|
fun switchUser(user: JellyfinUser) {
|
||||||
server: JellyfinServer,
|
|
||||||
user: JellyfinUser,
|
|
||||||
) {
|
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
try {
|
try {
|
||||||
serverRepository.changeUser(server, user)
|
serverRepository.changeUser(server, user)
|
||||||
|
|
@ -242,10 +202,7 @@ class SwitchUserViewModel
|
||||||
if (ex is InvalidStatusException && ex.status == 401) {
|
if (ex is InvalidStatusException && ex.status == 401) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
quickConnectState.value = null
|
quickConnectState.value = null
|
||||||
serverQuickConnect.value =
|
serverQuickConnect.value = false
|
||||||
serverQuickConnect.value!!.toMutableMap().apply {
|
|
||||||
put(server.id, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setError("Error with Quick Connect", ex)
|
setError("Error with Quick Connect", ex)
|
||||||
|
|
@ -258,55 +215,6 @@ class SwitchUserViewModel
|
||||||
quickConnectState.value = null
|
quickConnectState.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addServer(serverUrl: String) {
|
|
||||||
addServerState.value = LoadingState.Loading
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
try {
|
|
||||||
val serverInfo by jellyfin
|
|
||||||
.createApi(serverUrl)
|
|
||||||
.systemApi
|
|
||||||
.getPublicSystemInfo()
|
|
||||||
val id = serverInfo.id?.toUUIDOrNull()
|
|
||||||
if (id != null && serverInfo.startupWizardCompleted == true) {
|
|
||||||
serverRepository.addAndChangeServer(
|
|
||||||
JellyfinServer(
|
|
||||||
id = id,
|
|
||||||
name = serverInfo.serverName,
|
|
||||||
url = serverUrl,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val quickConnect =
|
|
||||||
jellyfin
|
|
||||||
.createApi(serverUrl)
|
|
||||||
.quickConnectApi
|
|
||||||
.getQuickConnectEnabled()
|
|
||||||
.content
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
serverQuickConnect.value =
|
|
||||||
serverQuickConnect.value!!.toMutableMap().apply {
|
|
||||||
put(id, quickConnect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
addServerState.value = LoadingState.Success
|
|
||||||
navigationManager.navigateTo(Destination.UserList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
addServerState.value =
|
|
||||||
LoadingState.Error("Server returned invalid response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Timber.w(ex, "Error creating API for $serverUrl")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
addServerState.value =
|
|
||||||
LoadingState.Error(exception = ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeUser(user: JellyfinUser) {
|
fun removeUser(user: JellyfinUser) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
serverRepository.removeUser(user)
|
serverRepository.removeUser(user)
|
||||||
|
|
@ -318,35 +226,6 @@ class SwitchUserViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeServer(server: JellyfinServer) {
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
serverRepository.removeServer(server)
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun discoverServers() {
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
jellyfin.discovery.discoverLocalServers().collect { server ->
|
|
||||||
val newServerList =
|
|
||||||
discoveredServers.value!!
|
|
||||||
.toMutableList()
|
|
||||||
.apply {
|
|
||||||
add(
|
|
||||||
JellyfinServer(
|
|
||||||
server.id.toUUID(),
|
|
||||||
server.name,
|
|
||||||
server.address,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
discoveredServers.value = newServerList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun setError(
|
private suspend fun setError(
|
||||||
msg: String? = null,
|
msg: String? = null,
|
||||||
ex: Exception? = null,
|
ex: Exception? = null,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.github.damontecres.wholphin.util
|
||||||
|
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
|
import org.jellyfin.sdk.model.api.UserItemDataDto
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface FavoriteWatchManager {
|
||||||
|
suspend fun setWatched(
|
||||||
|
itemId: UUID,
|
||||||
|
played: Boolean,
|
||||||
|
): UserItemDataDto
|
||||||
|
|
||||||
|
suspend fun setFavorite(
|
||||||
|
itemId: UUID,
|
||||||
|
favorite: Boolean,
|
||||||
|
): UserItemDataDto
|
||||||
|
}
|
||||||
|
|
||||||
|
class FavoriteWatchManagerImpl(
|
||||||
|
private val api: ApiClient,
|
||||||
|
) : FavoriteWatchManager {
|
||||||
|
override suspend fun setWatched(
|
||||||
|
itemId: UUID,
|
||||||
|
played: Boolean,
|
||||||
|
): UserItemDataDto =
|
||||||
|
if (played) {
|
||||||
|
api.playStateApi.markPlayedItem(itemId).content
|
||||||
|
} else {
|
||||||
|
api.playStateApi.markUnplayedItem(itemId).content
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setFavorite(
|
||||||
|
itemId: UUID,
|
||||||
|
favorite: Boolean,
|
||||||
|
): UserItemDataDto =
|
||||||
|
if (favorite) {
|
||||||
|
api.userLibraryApi.markFavoriteItem(itemId).content
|
||||||
|
} else {
|
||||||
|
api.userLibraryApi.unmarkFavoriteItem(itemId).content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ class PlaybackLifecycleObserver
|
||||||
constructor(
|
constructor(
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
private val playerFactory: PlayerFactory,
|
private val playerFactory: PlayerFactory,
|
||||||
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
) : DefaultLifecycleObserver {
|
) : DefaultLifecycleObserver {
|
||||||
private var wasPlaying: Boolean? = null
|
private var wasPlaying: Boolean? = null
|
||||||
|
|
||||||
|
|
@ -36,11 +37,13 @@ class PlaybackLifecycleObserver
|
||||||
wasPlaying = it.isPlaying
|
wasPlaying = it.isPlaying
|
||||||
it.pause()
|
it.pause()
|
||||||
}
|
}
|
||||||
|
themeSongPlayer.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
if (navigationManager.backStack.lastOrNull() is Destination.Playback) {
|
if (navigationManager.backStack.lastOrNull() is Destination.Playback) {
|
||||||
navigationManager.goBack()
|
navigationManager.goBack()
|
||||||
}
|
}
|
||||||
|
themeSongPlayer.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,17 @@ package com.github.damontecres.wholphin.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.hilt.IoCoroutineScope
|
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ActivityContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -20,24 +24,36 @@ import org.jellyfin.sdk.model.api.GeneralCommandType
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
@ActivityScoped
|
||||||
class ServerEventListener
|
class ServerEventListener
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ActivityContext private val context: Context,
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
@param:IoCoroutineScope private val scope: CoroutineScope,
|
private val serverRepository: ServerRepository,
|
||||||
) {
|
) : DefaultLifecycleObserver {
|
||||||
|
private val activity = (context as AppCompatActivity)
|
||||||
|
|
||||||
private var listenJob: Job? = null
|
private var listenJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
activity.lifecycle.addObserver(this)
|
||||||
|
serverRepository.current.observe(activity) {
|
||||||
|
Timber.d("New user/server: %s", it)
|
||||||
|
listenJob?.cancel()
|
||||||
|
if (it != null) {
|
||||||
|
init(it.server, it.user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
server: JellyfinServer?,
|
server: JellyfinServer?,
|
||||||
user: JellyfinUser?,
|
user: JellyfinUser?,
|
||||||
) {
|
) {
|
||||||
if (server != null && user != null && api.baseUrl != null && api.accessToken != null) {
|
if (server != null && user != null && api.baseUrl != null && api.accessToken != null) {
|
||||||
scope.launchIO {
|
(context as AppCompatActivity).lifecycleScope.launchIO {
|
||||||
api.sessionApi.postCapabilities(
|
api.sessionApi.postCapabilities(
|
||||||
playableMediaTypes = listOf(MediaType.VIDEO),
|
playableMediaTypes = listOf(MediaType.VIDEO),
|
||||||
supportedCommands =
|
supportedCommands =
|
||||||
|
|
@ -53,6 +69,7 @@ class ServerEventListener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupListeners() {
|
fun setupListeners() {
|
||||||
|
serverRepository.currentUser
|
||||||
Timber.v("Subscribing to WebSocket")
|
Timber.v("Subscribing to WebSocket")
|
||||||
listenJob?.cancel()
|
listenJob?.cancel()
|
||||||
listenJob =
|
listenJob =
|
||||||
|
|
@ -73,6 +90,20 @@ class ServerEventListener
|
||||||
.joinToString("\n")
|
.joinToString("\n")
|
||||||
showToast(context, toast, Toast.LENGTH_LONG)
|
showToast(context, toast, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
}.launchIn(scope)
|
}.launchIn(activity.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
|
serverRepository.current.value?.let { init(it.server, it.user) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
|
Timber.v("Cancelling WebSocket")
|
||||||
|
listenJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
|
Timber.v("Cancelling WebSocket")
|
||||||
|
listenJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ class ThemeSongPlayer
|
||||||
volumeLevel: ThemeSongVolume,
|
volumeLevel: ThemeSongVolume,
|
||||||
url: String,
|
url: String,
|
||||||
) {
|
) {
|
||||||
stop()
|
|
||||||
val volumeLevel =
|
val volumeLevel =
|
||||||
when (volumeLevel) {
|
when (volumeLevel) {
|
||||||
ThemeSongVolume.UNRECOGNIZED,
|
ThemeSongVolume.UNRECOGNIZED,
|
||||||
|
|
@ -92,6 +91,7 @@ class ThemeSongPlayer
|
||||||
ThemeSongVolume.HIGHEST -> 75f
|
ThemeSongVolume.HIGHEST -> 75f
|
||||||
}
|
}
|
||||||
player.apply {
|
player.apply {
|
||||||
|
stop()
|
||||||
volume = volumeLevel
|
volume = volumeLevel
|
||||||
setMediaItem(MediaItem.fromUri(url))
|
setMediaItem(MediaItem.fromUri(url))
|
||||||
prepare()
|
prepare()
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.playback.CurrentPlayback
|
import com.github.damontecres.wholphin.ui.playback.CurrentPlayback
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
|
|
@ -49,7 +49,7 @@ class TrackActivityPlaybackListener(
|
||||||
private var initialized = false
|
private var initialized = false
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
launch("reportPlaybackStart") {
|
||||||
Timber.v("reportPlaybackStart for ${itemPlayback.itemId}")
|
Timber.v("reportPlaybackStart for ${itemPlayback.itemId}")
|
||||||
api.playStateApi.reportPlaybackStart(
|
api.playStateApi.reportPlaybackStart(
|
||||||
PlaybackStartInfo(
|
PlaybackStartInfo(
|
||||||
|
|
@ -77,7 +77,7 @@ class TrackActivityPlaybackListener(
|
||||||
task.cancel()
|
task.cancel()
|
||||||
TIMER.purge()
|
TIMER.purge()
|
||||||
val position = player.currentPosition.milliseconds
|
val position = player.currentPosition.milliseconds
|
||||||
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
launch("reportPlaybackStopped") {
|
||||||
Timber.v("reportPlaybackStopped for ${itemPlayback.itemId} at $position")
|
Timber.v("reportPlaybackStopped for ${itemPlayback.itemId} at $position")
|
||||||
api.playStateApi.reportPlaybackStopped(
|
api.playStateApi.reportPlaybackStopped(
|
||||||
PlaybackStopInfo(
|
PlaybackStopInfo(
|
||||||
|
|
@ -107,7 +107,7 @@ class TrackActivityPlaybackListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveActivity(position: Long) {
|
private fun saveActivity(position: Long) {
|
||||||
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
launch("saveActivity") {
|
||||||
val calcPosition =
|
val calcPosition =
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
(if (position >= 0) position else player.currentPosition)
|
(if (position >= 0) position else player.currentPosition)
|
||||||
|
|
@ -135,6 +135,19 @@ class TrackActivityPlaybackListener(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launch(
|
||||||
|
name: String,
|
||||||
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
try {
|
||||||
|
block.invoke(this)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Exception during %s for %s", name, itemPlayback.itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "TrackActivityPlaybackListener"
|
private const val TAG = "TrackActivityPlaybackListener"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,7 @@
|
||||||
<string name="italic_font">Italicize font</string>
|
<string name="italic_font">Italicize font</string>
|
||||||
<string name="font">Font</string>
|
<string name="font">Font</string>
|
||||||
<string name="background">Background</string>
|
<string name="background">Background</string>
|
||||||
|
<string name="success">Success</string>
|
||||||
|
|
||||||
<plurals name="downloads">
|
<plurals name="downloads">
|
||||||
<item quantity="zero">%s downloads</item>
|
<item quantity="zero">%s downloads</item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue