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