Remember library/collection sorting (#150)

Closes #148 
Closes #83

Remember the chosen sort order & direction per user profile on device.
Also adds a few more sort options.

This is only saved on the device and not saved/loaded from the server.
This commit is contained in:
damontecres 2025-11-03 12:15:41 -05:00 committed by GitHub
parent 08577b8a31
commit 40267be633
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 486 additions and 51 deletions

View file

@ -0,0 +1,303 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "24390d64259f19e0e67ecfb827b974fb",
"entities": [
{
"tableName": "servers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "rowId",
"columnName": "rowId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"rowId"
]
},
"indices": [
{
"name": "index_users_id_serverId",
"unique": true,
"columnNames": [
"id",
"serverId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name": "index_users_id",
"unique": false,
"columnNames": [
"id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name": "index_users_serverId",
"unique": false,
"columnNames": [
"serverId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys": [
{
"table": "servers",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ItemPlayback",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "rowId",
"columnName": "rowId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourceId",
"columnName": "sourceId",
"affinity": "TEXT"
},
{
"fieldPath": "audioIndex",
"columnName": "audioIndex",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subtitleIndex",
"columnName": "subtitleIndex",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"rowId"
]
},
"indices": [
{
"name": "index_ItemPlayback_userId_itemId",
"unique": true,
"columnNames": [
"userId",
"itemId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"userId"
],
"referencedColumns": [
"rowId"
]
}
]
},
{
"tableName": "NavDrawerPinnedItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId",
"itemId"
]
},
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"userId"
],
"referencedColumns": [
"rowId"
]
}
]
},
{
"tableName": "LibraryDisplayInfo",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sort",
"columnName": "sort",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "direction",
"columnName": "direction",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId",
"itemId"
]
},
"indices": [
{
"name": "index_LibraryDisplayInfo_userId_itemId",
"unique": true,
"columnNames": [
"userId",
"itemId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"userId"
],
"referencedColumns": [
"rowId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '24390d64259f19e0e67ecfb827b974fb')"
]
}
}

View file

@ -10,17 +10,21 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID import java.util.UUID
@Database( @Database(
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class], entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
version = 5, version = 6,
exportSchema = true, exportSchema = true,
autoMigrations = [ autoMigrations = [
AutoMigration(3, 4), AutoMigration(3, 4),
AutoMigration(4, 5), AutoMigration(4, 5),
AutoMigration(5, 6),
], ],
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@ -30,6 +34,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun itemPlaybackDao(): ItemPlaybackDao abstract fun itemPlaybackDao(): ItemPlaybackDao
abstract fun serverPreferencesDao(): ServerPreferencesDao abstract fun serverPreferencesDao(): ServerPreferencesDao
abstract fun libraryDisplayInfoDao(): LibraryDisplayInfoDao
} }
class Converters { class Converters {
@ -38,6 +44,18 @@ class Converters {
@TypeConverter @TypeConverter
fun convertToUUID(str: String): UUID = str.toUUID() fun convertToUUID(str: String): UUID = str.toUUID()
@TypeConverter
fun convertItemSortBy(sort: ItemSortBy): String = sort.serialName
@TypeConverter
fun convertItemSortBy(sort: String): ItemSortBy? = ItemSortBy.fromNameOrNull(sort)
@TypeConverter
fun convertSortOrder(sort: SortOrder): String = sort.serialName
@TypeConverter
fun convertSortOrder(sort: String): SortOrder? = SortOrder.fromNameOrNull(sort)
} }
object Migrations { object Migrations {

View file

@ -0,0 +1,29 @@
package com.github.damontecres.wholphin.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import java.util.UUID
@Dao
interface LibraryDisplayInfoDao {
fun getItem(
user: JellyfinUser,
itemId: UUID,
): LibraryDisplayInfo? = getItem(user.rowId, itemId)
@Query("SELECT * from LibraryDisplayInfo WHERE userId=:userId AND itemId=:itemId")
fun getItem(
userId: Int,
itemId: UUID,
): LibraryDisplayInfo?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun saveItem(item: LibraryDisplayInfo): Long
@Query("SELECT * from LibraryDisplayInfo WHERE userId=:userId")
fun getItems(userId: Int): List<LibraryDisplayInfo>
}

View file

@ -0,0 +1,40 @@
@file:UseSerializers(UUIDSerializer::class)
package com.github.damontecres.wholphin.data.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.Index
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import java.util.UUID
@Entity(
foreignKeys = [
ForeignKey(
entity = JellyfinUser::class,
parentColumns = arrayOf("rowId"),
childColumns = arrayOf("userId"),
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
),
],
primaryKeys = ["userId", "itemId"],
indices = [Index("userId", "itemId", unique = true)],
)
@Serializable
data class LibraryDisplayInfo(
val userId: Int,
val itemId: UUID,
val sort: ItemSortBy,
val direction: SortOrder,
) {
@Ignore @Transient
val sortAndDirection = SortAndDirection(sort, direction)
}

View file

@ -9,6 +9,7 @@ import androidx.room.Room
import com.github.damontecres.wholphin.data.AppDatabase import com.github.damontecres.wholphin.data.AppDatabase
import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
import com.github.damontecres.wholphin.data.Migrations import com.github.damontecres.wholphin.data.Migrations
import com.github.damontecres.wholphin.data.ServerPreferencesDao import com.github.damontecres.wholphin.data.ServerPreferencesDao
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
@ -51,6 +52,10 @@ object DatabaseModule {
@Singleton @Singleton
fun serverPreferencesDao(db: AppDatabase): ServerPreferencesDao = db.serverPreferencesDao() fun serverPreferencesDao(db: AppDatabase): ServerPreferencesDao = db.serverPreferencesDao()
@Provides
@Singleton
fun libraryDisplayInfoDao(db: AppDatabase): LibraryDisplayInfoDao = db.libraryDisplayInfoDao()
@Provides @Provides
@Singleton @Singleton
fun userPreferencesDataStore( fun userPreferencesDataStore(

View file

@ -26,8 +26,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
@ -62,6 +65,8 @@ class CollectionFolderViewModel
@Inject @Inject
constructor( constructor(
api: ApiClient, api: ApiClient,
private val serverRepository: ServerRepository,
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
) : ItemViewModel(api) { ) : ItemViewModel(api) {
val loading = MutableLiveData<LoadingState>(LoadingState.Loading) val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val pager = MutableLiveData<List<BaseItem?>>(listOf()) val pager = MutableLiveData<List<BaseItem?>>(listOf())
@ -71,7 +76,7 @@ class CollectionFolderViewModel
fun init( fun init(
itemId: UUID?, itemId: UUID?,
potential: BaseItem?, potential: BaseItem?,
sortAndDirection: SortAndDirection, initialSortAndDirection: SortAndDirection?,
recursive: Boolean, recursive: Boolean,
filter: GetItemsFilter, filter: GetItemsFilter,
): Job = ): Job =
@ -82,12 +87,41 @@ class CollectionFolderViewModel
) + Dispatchers.IO, ) + Dispatchers.IO,
) { ) {
if (itemId != null) { if (itemId != null) {
fetchItem(itemId, potential) fetchItem(itemId)
val sortAndDirection =
if (initialSortAndDirection == null) {
serverRepository.currentUser?.let { user ->
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
} ?: SortAndDirection.DEFAULT
} else {
SortAndDirection.DEFAULT
}
loadResults(sortAndDirection, recursive, filter)
} }
loadResults(sortAndDirection, recursive, filter)
} }
fun loadResults( fun onSortChange(
sortAndDirection: SortAndDirection,
recursive: Boolean,
filter: GetItemsFilter,
) {
serverRepository.currentUser?.let { user ->
viewModelScope.launch(Dispatchers.IO) {
val libraryDisplayInfo =
LibraryDisplayInfo(
userId = user.rowId,
itemId = itemId,
sort = sortAndDirection.sort,
direction = sortAndDirection.direction,
)
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
}
}
loadResults(sortAndDirection, recursive, filter)
}
private fun loadResults(
sortAndDirection: SortAndDirection, sortAndDirection: SortAndDirection,
recursive: Boolean, recursive: Boolean,
filter: GetItemsFilter, filter: GetItemsFilter,
@ -126,17 +160,25 @@ class CollectionFolderViewModel
recursive = recursive, recursive = recursive,
excludeItemIds = item?.let { listOf(item.id) }, excludeItemIds = item?.let { listOf(item.id) },
sortBy = sortBy =
listOf( buildList {
sortAndDirection.sort, add(sortAndDirection.sort)
ItemSortBy.SORT_NAME, if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
ItemSortBy.PRODUCTION_YEAR, add(ItemSortBy.SORT_NAME)
), }
if (item?.data?.collectionType == CollectionType.MOVIES) {
add(ItemSortBy.PRODUCTION_YEAR)
}
},
sortOrder = sortOrder =
listOf( buildList {
sortAndDirection.direction, add(sortAndDirection.direction)
SortOrder.ASCENDING, if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
SortOrder.ASCENDING, add(SortOrder.ASCENDING)
), }
if (item?.data?.collectionType == CollectionType.MOVIES) {
add(SortOrder.ASCENDING)
}
},
fields = SlimItemFields, fields = SlimItemFields,
), ),
) )
@ -198,18 +240,14 @@ fun CollectionFolderGrid(
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: CollectionFolderViewModel = hiltViewModel(), viewModel: CollectionFolderViewModel = hiltViewModel(),
initialSortAndDirection: SortAndDirection = initialSortAndDirection: SortAndDirection? = null,
SortAndDirection(
ItemSortBy.SORT_NAME,
SortOrder.ASCENDING,
),
showTitle: Boolean = true, showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
) { ) {
OneTimeLaunchedEffect { OneTimeLaunchedEffect {
viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter) viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter)
} }
val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection) val sortAndDirection by viewModel.sortAndDirection.observeAsState()
val filter by viewModel.filter.observeAsState(initialFilter) val filter by viewModel.filter.observeAsState(initialFilter)
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val item by viewModel.item.observeAsState() val item by viewModel.item.observeAsState()
@ -226,11 +264,11 @@ fun CollectionFolderGrid(
preferences, preferences,
item, item,
pager, pager,
sortAndDirection = sortAndDirection, sortAndDirection = sortAndDirection!!,
modifier = modifier, modifier = modifier,
onClickItem = onClickItem, onClickItem = onClickItem,
onSortChange = { onSortChange = {
viewModel.loadResults(it, recursive, filter) viewModel.onSortChange(it, recursive, filter)
}, },
showTitle = showTitle, showTitle = showTitle,
positionCallback = positionCallback, positionCallback = positionCallback,

View file

@ -10,6 +10,14 @@ data class SortAndDirection(
val direction: SortOrder, val direction: SortOrder,
) { ) {
fun flip() = copy(direction = direction.flip()) fun flip() = copy(direction = direction.flip())
companion object {
val DEFAULT =
SortAndDirection(
ItemSortBy.SORT_NAME,
SortOrder.ASCENDING,
)
}
} }
fun SortOrder.flip() = if (this == SortOrder.ASCENDING) SortOrder.DESCENDING else SortOrder.ASCENDING fun SortOrder.flip() = if (this == SortOrder.ASCENDING) SortOrder.DESCENDING else SortOrder.ASCENDING
@ -20,6 +28,9 @@ val MovieSortOptions =
ItemSortBy.PREMIERE_DATE, ItemSortBy.PREMIERE_DATE,
ItemSortBy.DATE_CREATED, ItemSortBy.DATE_CREATED,
ItemSortBy.DATE_PLAYED, ItemSortBy.DATE_PLAYED,
ItemSortBy.COMMUNITY_RATING,
ItemSortBy.CRITIC_RATING,
ItemSortBy.PLAY_COUNT,
ItemSortBy.RANDOM, ItemSortBy.RANDOM,
) )
@ -30,6 +41,7 @@ val SeriesSortOptions =
ItemSortBy.DATE_CREATED, ItemSortBy.DATE_CREATED,
ItemSortBy.DATE_LAST_CONTENT_ADDED, ItemSortBy.DATE_LAST_CONTENT_ADDED,
ItemSortBy.DATE_PLAYED, ItemSortBy.DATE_PLAYED,
ItemSortBy.COMMUNITY_RATING,
ItemSortBy.RANDOM, ItemSortBy.RANDOM,
) )
@ -50,5 +62,8 @@ fun getStringRes(sort: ItemSortBy): Int =
ItemSortBy.DATE_LAST_CONTENT_ADDED -> R.string.sort_by_date_episode_added ItemSortBy.DATE_LAST_CONTENT_ADDED -> R.string.sort_by_date_episode_added
ItemSortBy.DATE_PLAYED -> R.string.sort_by_date_played ItemSortBy.DATE_PLAYED -> R.string.sort_by_date_played
ItemSortBy.RANDOM -> R.string.sort_by_random ItemSortBy.RANDOM -> R.string.sort_by_random
ItemSortBy.COMMUNITY_RATING -> R.string.community_rating
ItemSortBy.CRITIC_RATING -> R.string.critic_rating
ItemSortBy.PLAY_COUNT -> R.string.play_count
else -> throw IllegalArgumentException("Unsupported sort option: $sort") else -> throw IllegalArgumentException("Unsupported sort option: $sort")
} }

View file

@ -25,21 +25,11 @@ abstract class ItemViewModel(
val api: ApiClient, val api: ApiClient,
) : ViewModel() { ) : ViewModel() {
val item = MutableLiveData<BaseItem?>(null) val item = MutableLiveData<BaseItem?>(null)
lateinit var itemId: UUID
suspend fun fetchItem( suspend fun fetchItem(itemId: UUID): BaseItem =
itemId: UUID,
potential: BaseItem?,
): BaseItem =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// val fetchedItem = this@ItemViewModel.itemId = itemId
// when {
// item.value == null && potential?.id == itemId -> potential
// item.value?.id == itemId -> item.value
// else -> {
// val it = api.userLibraryApi.getItem(itemId).content
// BaseItem.from(it, api)
// }
// }
val it = api.userLibraryApi.getItem(itemId).content val it = api.userLibraryApi.getItem(itemId).content
val fetchedItem = BaseItem.from(it, api) val fetchedItem = BaseItem.from(it, api)
return@withContext fetchedItem.let { return@withContext fetchedItem.let {
@ -89,10 +79,8 @@ abstract class LoadingItemViewModel(
open suspend fun fetchAndSetItem(itemId: UUID): BaseItem = open suspend fun fetchAndSetItem(itemId: UUID): BaseItem =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val fetchedItem = api.userLibraryApi.getItem(itemId).content val item = fetchItem(itemId)
val item = BaseItem.from(fetchedItem, api)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@LoadingItemViewModel.item.value = item
loading.value = LoadingState.Success loading.value = LoadingState.Success
} }
return@withContext item return@withContext item

View file

@ -102,7 +102,7 @@ class PlaylistViewModel
Dispatchers.IO + Dispatchers.IO +
LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"), LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"),
) { ) {
val playlist = fetchItem(playlistId, null) val playlist = fetchItem(playlistId)
val request = val request =
GetPlaylistItemsRequest( GetPlaylistItemsRequest(
playlistId = playlist.id, playlistId = playlist.id,

View file

@ -48,17 +48,14 @@ class MovieViewModel
val itemPlaybackRepository: ItemPlaybackRepository, val itemPlaybackRepository: ItemPlaybackRepository,
private val themeSongPlayer: ThemeSongPlayer, private val themeSongPlayer: ThemeSongPlayer,
) : LoadingItemViewModel(api) { ) : LoadingItemViewModel(api) {
private lateinit var itemId: UUID
val trailers = MutableLiveData<List<Trailer>>(listOf()) val trailers = MutableLiveData<List<Trailer>>(listOf())
val people = MutableLiveData<List<Person>>(listOf()) val people = MutableLiveData<List<Person>>(listOf())
val chapters = MutableLiveData<List<Chapter>>(listOf()) val chapters = MutableLiveData<List<Chapter>>(listOf())
val similar = MutableLiveData<List<BaseItem>>() val similar = MutableLiveData<List<BaseItem>>()
val chosenStreams = MutableLiveData<ChosenStreams?>(null) val chosenStreams = MutableLiveData<ChosenStreams?>(null)
fun init(itemId: UUID): Job? { fun init(itemId: UUID): Job? =
this.itemId = itemId viewModelScope.launch(
return viewModelScope.launch(
Dispatchers.IO + Dispatchers.IO +
LoadingExceptionHandler( LoadingExceptionHandler(
loading, loading,
@ -124,7 +121,6 @@ class MovieViewModel
} }
} }
} }
}
fun setWatched(played: Boolean) = fun setWatched(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {

View file

@ -84,7 +84,7 @@ class SeriesViewModel
"Error loading series $seriesId", "Error loading series $seriesId",
) + Dispatchers.IO, ) + Dispatchers.IO,
) { ) {
val item = fetchItem(seriesId, potential) val item = fetchItem(seriesId)
val seasons = getSeasons(item) val seasons = getSeasons(item)
// If a particular season was requested, fetch those episodes, otherwise get the first season // If a particular season was requested, fetch those episodes, otherwise get the first season
@ -263,7 +263,7 @@ class SeriesViewModel
if (listIndex != null) { if (listIndex != null) {
refreshEpisode(itemId, listIndex) refreshEpisode(itemId, listIndex)
} else { } else {
fetchItem(seriesId, null) fetchItem(seriesId)
} }
} }
@ -272,7 +272,7 @@ class SeriesViewModel
played: Boolean, played: Boolean,
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { ) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
setWatched(seasonId, played, null) setWatched(seasonId, played, null)
val series = fetchItem(seriesId, null) val series = fetchItem(seriesId)
val seasons = getSeasons(series) val seasons = getSeasons(series)
this@SeriesViewModel.seasons.setValueOnMain(seasons) this@SeriesViewModel.seasons.setValueOnMain(seasons)
} }
@ -284,7 +284,7 @@ class SeriesViewModel
} else { } else {
api.playStateApi.markUnplayedItem(seriesId) api.playStateApi.markUnplayedItem(seriesId)
} }
val series = fetchItem(seriesId, null) val series = fetchItem(seriesId)
val seasons = getSeasons(series) val seasons = getSeasons(series)
this@SeriesViewModel.seasons.setValueOnMain(seasons) this@SeriesViewModel.seasons.setValueOnMain(seasons)
} }

View file

@ -102,5 +102,8 @@
<string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string> <string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string>
<string name="show_next_up_when">Show next up</string> <string name="show_next_up_when">Show next up</string>
<string name="next_up">Next Up</string> <string name="next_up">Next Up</string>
<string name="community_rating">Community Rating</string>
<string name="critic_rating">Critic Rating</string>
<string name="play_count">Play Count</string>
</resources> </resources>