mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add ability to customize the navigation drawer menu (#60)
Adds support for customizing the items listed on the left side navigation drawer menu. Each item can be toggled on/off in settings. The hidden ones are still accessible under "More" on the nav drawer, which only appears if some items are hidden. Additionally, if any of menu items are hidden, their "latest" content will not appear on the home page. The setting is stored per user profile.
This commit is contained in:
parent
27d0a17a00
commit
fc4954b561
17 changed files with 776 additions and 184 deletions
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "59351f2b9819f90fab82ff0f353c79a5",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '59351f2b9819f90fab82ff0f353c79a5')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -8,20 +8,26 @@ import androidx.room.TypeConverters
|
|||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import java.util.UUID
|
||||
|
||||
@Database(
|
||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class],
|
||||
version = 4,
|
||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class],
|
||||
version = 5,
|
||||
exportSchema = true,
|
||||
autoMigrations = [AutoMigration(3, 4)],
|
||||
autoMigrations = [
|
||||
AutoMigration(3, 4),
|
||||
AutoMigration(4, 5),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun serverDao(): JellyfinServerDao
|
||||
|
||||
abstract fun itemPlaybackDao(): ItemPlaybackDao
|
||||
|
||||
abstract fun serverPreferencesDao(): ServerPreferencesDao
|
||||
}
|
||||
|
||||
class Converters {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ interface JellyfinServerDao {
|
|||
fun updateUser(user: JellyfinUser): Int
|
||||
|
||||
@Transaction
|
||||
fun addOrUpdateUser(user: JellyfinUser) {
|
||||
fun addOrUpdateUser(user: JellyfinUser): JellyfinUser {
|
||||
val result = addUser(user)
|
||||
if (result == -1L) {
|
||||
val toSave =
|
||||
|
|
@ -92,7 +92,9 @@ interface JellyfinServerDao {
|
|||
user
|
||||
}
|
||||
updateUser(toSave)
|
||||
return toSave
|
||||
}
|
||||
return user.copy(rowId = result.toInt())
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM users WHERE serverId = :serverId AND id = :userId")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
package com.github.damontecres.wholphin.data
|
||||
|
||||
import android.content.Context
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||
import com.github.damontecres.wholphin.util.supportedCollectionTypes
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NavDrawerItemRepository
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val serverPreferencesDao: ServerPreferencesDao,
|
||||
) {
|
||||
suspend fun getNavDrawerItems(): List<NavDrawerItem> {
|
||||
val user = serverRepository.currentUser
|
||||
val userViews =
|
||||
api.userViewsApi
|
||||
.getUserViews(userId = user?.id)
|
||||
.content.items
|
||||
|
||||
val builtins = listOf(NavDrawerItem.Favorites)
|
||||
val libraries =
|
||||
userViews
|
||||
.filter { it.collectionType in supportedCollectionTypes }
|
||||
.map {
|
||||
ServerNavDrawerItem(
|
||||
itemId = it.id,
|
||||
name = it.name ?: it.id.toString(),
|
||||
destination = BaseItem.from(it, api).destination(),
|
||||
type = it.collectionType ?: CollectionType.UNKNOWN,
|
||||
)
|
||||
}
|
||||
return builtins + libraries
|
||||
}
|
||||
|
||||
suspend fun getFilteredNavDrawerItems(items: List<NavDrawerItem>): List<NavDrawerItem> {
|
||||
val user = serverRepository.currentUser
|
||||
val navDrawerPins =
|
||||
user
|
||||
?.let {
|
||||
serverPreferencesDao.getNavDrawerPinnedItems(it)
|
||||
}.orEmpty()
|
||||
val filtered = items.filter { navDrawerPins.isPinned(it.id) }
|
||||
if (items.size != filtered.size) {
|
||||
// Some were filtered out, check if should include More
|
||||
if (navDrawerPins.isPinned(NavDrawerItem.More.id)) {
|
||||
return filtered + listOf(NavDrawerItem.More)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
}
|
||||
|
||||
fun List<NavDrawerPinnedItem>.isPinned(id: String) = (firstOrNull { it.itemId == id }?.type ?: NavPinType.PINNED) == NavPinType.PINNED
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
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.NavDrawerPinnedItem
|
||||
|
||||
@Dao
|
||||
interface ServerPreferencesDao {
|
||||
fun getNavDrawerPinnedItems(user: JellyfinUser): List<NavDrawerPinnedItem> = getNavDrawerPinnedItems(user.rowId)
|
||||
|
||||
@Query("SELECT * from NavDrawerPinnedItem WHERE userId=:userId")
|
||||
fun getNavDrawerPinnedItems(userId: Int): List<NavDrawerPinnedItem>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun saveNavDrawerPinnedItems(vararg items: NavDrawerPinnedItem)
|
||||
}
|
||||
|
|
@ -82,15 +82,13 @@ class ServerRepository
|
|||
val sysInfo by apiClient.systemApi.getSystemInfo()
|
||||
|
||||
val updatedServer = server.copy(name = sysInfo.serverName)
|
||||
val updatedUser =
|
||||
var updatedUser =
|
||||
user.copy(
|
||||
id = userDto.id,
|
||||
name = userDto.name,
|
||||
)
|
||||
if (updatedUser.rowId <= 0) {
|
||||
}
|
||||
serverDao.addOrUpdateServer(updatedServer)
|
||||
serverDao.addOrUpdateUser(updatedUser)
|
||||
updatedUser = serverDao.addOrUpdateUser(updatedUser)
|
||||
userPreferencesDataStore.updateData {
|
||||
it
|
||||
.toBuilder()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import com.github.damontecres.wholphin.data.JellyfinUser
|
||||
|
||||
enum class NavPinType {
|
||||
PINNED,
|
||||
UNPINNED,
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = JellyfinUser::class,
|
||||
parentColumns = arrayOf("rowId"),
|
||||
childColumns = arrayOf("userId"),
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
primaryKeys = ["userId", "itemId"],
|
||||
)
|
||||
data class NavDrawerPinnedItem(
|
||||
val userId: Int,
|
||||
val itemId: String,
|
||||
val type: NavPinType,
|
||||
)
|
||||
|
|
@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.data.AppDatabase
|
|||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||
import com.github.damontecres.wholphin.data.Migrations
|
||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
||||
import dagger.Module
|
||||
|
|
@ -46,6 +47,10 @@ object DatabaseModule {
|
|||
@Singleton
|
||||
fun itemPlaybackDao(db: AppDatabase): ItemPlaybackDao = db.itemPlaybackDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun serverPreferencesDao(db: AppDatabase): ServerPreferencesDao = db.serverPreferencesDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun userPreferencesDataStore(
|
||||
|
|
|
|||
|
|
@ -526,6 +526,14 @@ sealed interface AppPreference<T> {
|
|||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
|
||||
val UserPinnedNavDrawerItems =
|
||||
AppClickablePreference(
|
||||
title = R.string.nav_drawer_pins,
|
||||
summary = R.string.nav_drawer_pins_summary,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -556,6 +564,13 @@ val basicPreferences =
|
|||
AppPreference.SkipBackOnResume,
|
||||
),
|
||||
),
|
||||
PreferenceGroup(
|
||||
title = R.string.profile_specific_settings,
|
||||
preferences =
|
||||
listOf(
|
||||
AppPreference.UserPinnedNavDrawerItems,
|
||||
),
|
||||
),
|
||||
PreferenceGroup(
|
||||
title = R.string.about,
|
||||
preferences =
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ package com.github.damontecres.wholphin.ui.main
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||
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.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||
|
|
@ -19,7 +22,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.tvShowsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
|
|
@ -36,6 +38,8 @@ class HomeViewModel
|
|||
constructor(
|
||||
val api: ApiClient,
|
||||
val navigationManager: NavigationManager,
|
||||
val serverRepository: ServerRepository,
|
||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
) : ViewModel() {
|
||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val homeRows = MutableLiveData<List<HomeRow>>()
|
||||
|
|
@ -52,64 +56,44 @@ class HomeViewModel
|
|||
),
|
||||
) {
|
||||
Timber.d("init HomeViewModel")
|
||||
val user by api.userApi.getCurrentUser()
|
||||
// val displayPrefs =
|
||||
// api.displayPreferencesApi
|
||||
// .getDisplayPreferences(
|
||||
// displayPreferencesId = "usersettings",
|
||||
// client = "emby",
|
||||
// ).content
|
||||
val homeSections =
|
||||
listOf(
|
||||
HomeSection.RESUME,
|
||||
HomeSection.NEXT_UP,
|
||||
HomeSection.LATEST_MEDIA,
|
||||
)
|
||||
// TODO use display preferences?
|
||||
|
||||
// displayPrefs.customPrefs.entries
|
||||
// .filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() }
|
||||
// .sortedBy { it.key }
|
||||
// .map { HomeSection.fromKey(it.value ?: "") }
|
||||
// .filter {
|
||||
// it in
|
||||
// setOf(
|
||||
// HomeSection.LATEST_MEDIA,
|
||||
// HomeSection.NEXT_UP,
|
||||
// HomeSection.RESUME,
|
||||
// )
|
||||
// }
|
||||
serverRepository.currentUserDto?.let { userDto ->
|
||||
val includedIds =
|
||||
navDrawerItemRepository
|
||||
.getFilteredNavDrawerItems(navDrawerItemRepository.getNavDrawerItems())
|
||||
.filter { it is ServerNavDrawerItem }
|
||||
.map { (it as ServerNavDrawerItem).itemId }
|
||||
// TODO data is fetched all together which may be slow for large servers
|
||||
val resume = getResume(userDto.id, limit)
|
||||
val nextUp = getNextUp(userDto.id, limit, prefs.enableRewatchingNextUp)
|
||||
val latest = getLatest(userDto, limit, includedIds)
|
||||
|
||||
// TODO data is fetched all together which may be slow for large servers
|
||||
val resume = getResume(user.id, limit)
|
||||
val nextUp = getNextUp(user.id, limit, prefs.enableRewatchingNextUp)
|
||||
val latest = getLatest(user, limit)
|
||||
|
||||
val homeRows =
|
||||
if (prefs.combineContinueNext) {
|
||||
listOf(
|
||||
HomeRow(
|
||||
section = HomeSection.NEXT_UP,
|
||||
items = resume + nextUp,
|
||||
),
|
||||
*latest.toTypedArray(),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
HomeRow(
|
||||
section = HomeSection.RESUME,
|
||||
items = resume,
|
||||
),
|
||||
HomeRow(
|
||||
section = HomeSection.NEXT_UP,
|
||||
items = nextUp,
|
||||
),
|
||||
*latest.toTypedArray(),
|
||||
)
|
||||
val homeRows =
|
||||
if (prefs.combineContinueNext) {
|
||||
listOf(
|
||||
HomeRow(
|
||||
section = HomeSection.NEXT_UP,
|
||||
items = resume + nextUp,
|
||||
),
|
||||
*latest.toTypedArray(),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
HomeRow(
|
||||
section = HomeSection.RESUME,
|
||||
items = resume,
|
||||
),
|
||||
HomeRow(
|
||||
section = HomeSection.NEXT_UP,
|
||||
items = nextUp,
|
||||
),
|
||||
*latest.toTypedArray(),
|
||||
)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
this@HomeViewModel.homeRows.value = homeRows
|
||||
loadingState.value = LoadingState.Success
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
this@HomeViewModel.homeRows.value = homeRows
|
||||
loadingState.value = LoadingState.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,11 +146,16 @@ class HomeViewModel
|
|||
private suspend fun getLatest(
|
||||
user: UserDto,
|
||||
limit: Int,
|
||||
includedIds: List<UUID>,
|
||||
): List<HomeRow> {
|
||||
val latestMediaIncludes =
|
||||
user.configuration?.orderedViews.orEmpty().toMutableList().apply {
|
||||
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
||||
}
|
||||
user.configuration
|
||||
?.orderedViews
|
||||
.orEmpty()
|
||||
.toMutableList()
|
||||
.apply {
|
||||
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
||||
}.filter { includedIds.contains(it) }
|
||||
|
||||
val views by api.userViewsApi.getUserViews()
|
||||
val rows =
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.ui.nav
|
|||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
|
@ -17,10 +16,13 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -49,6 +51,7 @@ import androidx.tv.material3.LocalContentColor
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.NavigationDrawer
|
||||
import androidx.tv.material3.NavigationDrawerItem
|
||||
import androidx.tv.material3.NavigationDrawerItemDefaults
|
||||
import androidx.tv.material3.NavigationDrawerScope
|
||||
import androidx.tv.material3.ProvideTextStyle
|
||||
import androidx.tv.material3.Text
|
||||
|
|
@ -57,26 +60,20 @@ import androidx.tv.material3.surfaceColorAtElevation
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||
import com.github.damontecres.wholphin.ui.spacedByWithFooter
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.supportedCollectionTypes
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
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.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -84,45 +81,25 @@ import javax.inject.Inject
|
|||
class NavDrawerViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext val context: Context,
|
||||
val serverRepository: ServerRepository,
|
||||
val api: ApiClient,
|
||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
val navigationManager: NavigationManager,
|
||||
) : ViewModel() {
|
||||
private var all: List<NavDrawerItem>? = null
|
||||
val moreLibraries = MutableLiveData<List<NavDrawerItem>>(null)
|
||||
val libraries = MutableLiveData<List<NavDrawerItem>>(listOf())
|
||||
val selectedIndex = MutableLiveData<Int>(-1)
|
||||
val selectedIndex = MutableLiveData(-1)
|
||||
val showMore = MutableLiveData(false)
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launchIO {
|
||||
val all = all ?: navDrawerItemRepository.getNavDrawerItems()
|
||||
this@NavDrawerViewModel.all = all
|
||||
val libraries = navDrawerItemRepository.getFilteredNavDrawerItems(all)
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler(true)) {
|
||||
val userViews =
|
||||
api.userViewsApi
|
||||
.getUserViews()
|
||||
.content.items
|
||||
val libraries =
|
||||
userViews
|
||||
.filter { it.collectionType in supportedCollectionTypes }
|
||||
.map {
|
||||
NavDrawerItem(
|
||||
id = it.id,
|
||||
name = it.name ?: it.id.toString(),
|
||||
destination = BaseItem.from(it, api).destination(),
|
||||
type = it.collectionType ?: CollectionType.UNKNOWN,
|
||||
iconStringRes = null,
|
||||
)
|
||||
}
|
||||
val extra =
|
||||
listOf(
|
||||
NavDrawerItem(
|
||||
id = UUID.randomUUID(),
|
||||
name = context.getString(R.string.favorites),
|
||||
destination = Destination.Favorites,
|
||||
type = CollectionType.UNKNOWN,
|
||||
iconStringRes = R.string.fa_heart,
|
||||
),
|
||||
)
|
||||
Timber.d("Got ${userViews.size} user views filtered to ${libraries.size}")
|
||||
withContext(Dispatchers.Main) {
|
||||
this@NavDrawerViewModel.libraries.value = extra + libraries
|
||||
this@NavDrawerViewModel.moreLibraries.value =
|
||||
all.toMutableList().apply { removeAll(libraries) }
|
||||
this@NavDrawerViewModel.libraries.value = libraries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,15 +107,42 @@ class NavDrawerViewModel
|
|||
fun setIndex(index: Int) {
|
||||
selectedIndex.value = index
|
||||
}
|
||||
|
||||
fun setShowMore(value: Boolean) {
|
||||
showMore.value = value
|
||||
}
|
||||
}
|
||||
|
||||
data class NavDrawerItem(
|
||||
val id: UUID,
|
||||
sealed interface NavDrawerItem {
|
||||
val id: String
|
||||
|
||||
fun name(context: Context): String
|
||||
|
||||
object Favorites : NavDrawerItem {
|
||||
override val id: String
|
||||
get() = "a_favorites"
|
||||
|
||||
override fun name(context: Context): String = context.getString(R.string.favorites)
|
||||
}
|
||||
|
||||
object More : NavDrawerItem {
|
||||
override val id: String
|
||||
get() = "a_more"
|
||||
|
||||
override fun name(context: Context): String = context.getString(R.string.more)
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerNavDrawerItem(
|
||||
val itemId: UUID,
|
||||
val name: String,
|
||||
val destination: Destination,
|
||||
val type: CollectionType,
|
||||
@param:StringRes val iconStringRes: Int?,
|
||||
)
|
||||
) : NavDrawerItem {
|
||||
override val id: String = "s_" + itemId.toServerString()
|
||||
|
||||
override fun name(context: Context): String = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the left side navigation drawer with [DestinationContent] on the right
|
||||
|
|
@ -162,17 +166,49 @@ fun NavDrawer(
|
|||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val drawerFocusRequester = remember { FocusRequester() }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
// If the user presses back while on the home page, open the nav drawer, another back press will quit the app
|
||||
BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination is Destination.Home)) {
|
||||
drawerState.setValue(DrawerValue.Open)
|
||||
drawerFocusRequester.requestFocus()
|
||||
}
|
||||
val moreLibraries by viewModel.moreLibraries.observeAsState(listOf())
|
||||
val libraries by viewModel.libraries.observeAsState(listOf())
|
||||
LaunchedEffect(Unit) { viewModel.init() }
|
||||
|
||||
val showMore by viewModel.showMore.observeAsState(false)
|
||||
// val libraries = if (showPinnedOnly) pinnedLibraries else allLibraries
|
||||
// A negative index is a built in page, >=0 is a library
|
||||
val selectedIndex by viewModel.selectedIndex.observeAsState(-1)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
fun setShowMore(value: Boolean) {
|
||||
viewModel.setShowMore(value)
|
||||
}
|
||||
|
||||
BackHandler(enabled = showMore && drawerState.currentValue == DrawerValue.Open) {
|
||||
setShowMore(false)
|
||||
}
|
||||
|
||||
val onClick = { index: Int, item: NavDrawerItem ->
|
||||
when (item) {
|
||||
NavDrawerItem.Favorites -> {
|
||||
viewModel.setIndex(index)
|
||||
viewModel.navigationManager.navigateToFromDrawer(
|
||||
Destination.Favorites,
|
||||
)
|
||||
}
|
||||
|
||||
NavDrawerItem.More -> {
|
||||
setShowMore(!showMore)
|
||||
}
|
||||
|
||||
is ServerNavDrawerItem -> {
|
||||
viewModel.setIndex(index)
|
||||
viewModel.navigationManager.navigateToFromDrawer(item.destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigationDrawer(
|
||||
modifier =
|
||||
|
|
@ -205,6 +241,7 @@ fun NavDrawer(
|
|||
onClick = {
|
||||
viewModel.navigationManager.navigateTo(Destination.UserList)
|
||||
},
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
|
@ -217,10 +254,11 @@ fun NavDrawer(
|
|||
viewModel.navigationManager.navigateToFromDrawer(Destination.Search)
|
||||
},
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
selectedIndex == -2,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == -2,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).animateItem(),
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
|
@ -237,27 +275,44 @@ fun NavDrawer(
|
|||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
selectedIndex == -1,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == -1,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).animateItem(),
|
||||
)
|
||||
}
|
||||
itemsIndexed(libraries) { index, it ->
|
||||
NavItem(
|
||||
library = it,
|
||||
selected = selectedIndex == index,
|
||||
onClick = {
|
||||
viewModel.setIndex(index)
|
||||
viewModel.navigationManager.navigateToFromDrawer(it.destination)
|
||||
},
|
||||
moreExpanded = showMore,
|
||||
onClick = { onClick.invoke(index, it) },
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
selectedIndex == index,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == index,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).animateItem(),
|
||||
)
|
||||
}
|
||||
if (showMore) {
|
||||
itemsIndexed(moreLibraries) { index, it ->
|
||||
NavItem(
|
||||
library = it,
|
||||
selected = selectedIndex == (index + libraries.size),
|
||||
moreExpanded = showMore,
|
||||
onClick = { onClick.invoke(index + libraries.size, it) },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
selectedIndex == (index + libraries.size),
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
IconNavItem(
|
||||
text = "Settings",
|
||||
|
|
@ -270,7 +325,7 @@ fun NavDrawer(
|
|||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier,
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -339,20 +394,29 @@ fun NavigationDrawerScope.NavItem(
|
|||
library: NavDrawerItem,
|
||||
onClick: () -> Unit,
|
||||
selected: Boolean,
|
||||
moreExpanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
containerColor: Color = Color.Unspecified,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val icon =
|
||||
when (library.type) {
|
||||
CollectionType.MOVIES -> R.string.fa_film
|
||||
CollectionType.TVSHOWS -> R.string.fa_tv
|
||||
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
||||
CollectionType.LIVETV -> R.string.fa_tv
|
||||
CollectionType.MUSIC -> R.string.fa_music
|
||||
CollectionType.BOXSETS -> R.string.fa_open_folder
|
||||
CollectionType.PLAYLISTS -> R.string.fa_list_ul
|
||||
else -> library.iconStringRes
|
||||
} ?: R.string.fa_film
|
||||
when (library) {
|
||||
NavDrawerItem.Favorites -> R.string.fa_heart
|
||||
NavDrawerItem.More -> R.string.fa_ellipsis
|
||||
|
||||
is ServerNavDrawerItem ->
|
||||
when (library.type) {
|
||||
CollectionType.MOVIES -> R.string.fa_film
|
||||
CollectionType.TVSHOWS -> R.string.fa_tv
|
||||
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
||||
CollectionType.LIVETV -> R.string.fa_tv
|
||||
CollectionType.MUSIC -> R.string.fa_music
|
||||
CollectionType.BOXSETS -> R.string.fa_open_folder
|
||||
CollectionType.PLAYLISTS -> R.string.fa_list_ul
|
||||
else -> R.string.fa_film
|
||||
}
|
||||
}
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val color =
|
||||
when {
|
||||
|
|
@ -364,6 +428,10 @@ fun NavigationDrawerScope.NavItem(
|
|||
modifier = modifier,
|
||||
selected = false,
|
||||
onClick = onClick,
|
||||
colors =
|
||||
NavigationDrawerItemDefaults.colors(
|
||||
containerColor = containerColor,
|
||||
),
|
||||
leadingContent = {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
|
|
@ -375,11 +443,19 @@ fun NavigationDrawerScope.NavItem(
|
|||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
if (library is NavDrawerItem.More) {
|
||||
Icon(
|
||||
imageVector = if (moreExpanded) Icons.Default.ArrowDropDown else Icons.Default.KeyboardArrowLeft,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
interactionSource = null,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier,
|
||||
text = library.name ?: library.id.toString(),
|
||||
text = library.name(context),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Switch
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
|
|
@ -53,6 +52,7 @@ import com.github.damontecres.wholphin.ui.components.EditTextBox
|
|||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.SortedSet
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
|
|
@ -197,7 +197,7 @@ fun <T> ComposablePreference(
|
|||
}
|
||||
|
||||
is AppMultiChoicePreference<*> -> {
|
||||
val values = stringArrayResource(preference.displayValues).toList()
|
||||
val values = stringArrayResource(preference.displayValues).toSortedSet()
|
||||
val summary =
|
||||
preference.summary?.let { stringResource(it) }
|
||||
?: preference.summary(context, value)
|
||||
|
|
@ -207,46 +207,14 @@ fun <T> ComposablePreference(
|
|||
list.addAll(value as List<Any>)
|
||||
list
|
||||
}
|
||||
|
||||
val onClick = { item: Any ->
|
||||
if (selectedValues.contains(item)) {
|
||||
selectedValues.remove(item)
|
||||
} else {
|
||||
selectedValues.add(item)
|
||||
}
|
||||
onValueChange.invoke(selectedValues.toList() as T)
|
||||
}
|
||||
|
||||
ClickPreference(
|
||||
MultiChoicePreference(
|
||||
possibleValues = values as SortedSet<Any>,
|
||||
selectedValues = selectedValues,
|
||||
title = title,
|
||||
summary = summary,
|
||||
onClick = {
|
||||
dialogParams =
|
||||
DialogParams(
|
||||
title = title,
|
||||
fromLongClick = false,
|
||||
items =
|
||||
values.mapIndexed { index, it ->
|
||||
val item = preference.allValues[index]!!
|
||||
DialogItem(
|
||||
headlineContent = { Text(it) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = selectedValues.contains(item),
|
||||
onCheckedChange = {
|
||||
onClick.invoke(item)
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onClick.invoke(item)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
onValueChange = {
|
||||
onValueChange.invoke(selectedValues.toList() as T)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
package com.github.damontecres.wholphin.ui.preferences
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableStateSetOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.tv.material3.Switch
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
|
||||
@Composable
|
||||
fun <T> MultiChoicePreference(
|
||||
title: String,
|
||||
summary: String?,
|
||||
possibleValues: Set<T>,
|
||||
selectedValues: Set<T>,
|
||||
onValueChange: (List<T>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
valueDisplay: @Composable (item: T) -> Unit = { Text(it.toString()) },
|
||||
) {
|
||||
// val values = stringArrayResource(preference.displayValues).toList()
|
||||
// val summary =
|
||||
// preference.summary?.let { stringResource(it) }
|
||||
// ?: preference.summary(context, value)
|
||||
val selectedValues =
|
||||
remember {
|
||||
val list = mutableStateSetOf<T>()
|
||||
list.addAll(selectedValues)
|
||||
list
|
||||
}
|
||||
|
||||
val onClick = { item: T ->
|
||||
if (selectedValues.contains(item)) {
|
||||
selectedValues.remove(item)
|
||||
} else {
|
||||
selectedValues.add(item)
|
||||
}
|
||||
onValueChange.invoke(selectedValues.toList())
|
||||
}
|
||||
|
||||
var dialogParams by remember { mutableStateOf<DialogParams?>(null) }
|
||||
|
||||
ClickPreference(
|
||||
title = title,
|
||||
summary = summary,
|
||||
onClick = {
|
||||
dialogParams =
|
||||
DialogParams(
|
||||
title = title,
|
||||
fromLongClick = false,
|
||||
items =
|
||||
possibleValues.mapIndexed { index, item ->
|
||||
DialogItem(
|
||||
headlineContent = { valueDisplay.invoke(item) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = selectedValues.contains(item),
|
||||
onCheckedChange = {
|
||||
onClick.invoke(item)
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onClick.invoke(item)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
AnimatedVisibility(dialogParams != null) {
|
||||
dialogParams?.let {
|
||||
DialogPopup(
|
||||
showDialog = true,
|
||||
title = it.title,
|
||||
dialogItems = it.items,
|
||||
onDismissRequest = { dialogParams = null },
|
||||
waitToLoad = false,
|
||||
dismissOnClick = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,9 @@ fun PreferencesContent(
|
|||
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
||||
val state = rememberLazyListState()
|
||||
var preferences by remember { mutableStateOf(initialPreferences) }
|
||||
|
||||
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.preferenceDataStore.data.collect {
|
||||
preferences = it
|
||||
|
|
@ -262,6 +265,24 @@ fun PreferencesContent(
|
|||
)
|
||||
}
|
||||
|
||||
AppPreference.UserPinnedNavDrawerItems -> {
|
||||
val selectedItems =
|
||||
navDrawerPins.keys.mapNotNull {
|
||||
if (navDrawerPins[it] ?: false) it else null
|
||||
}
|
||||
MultiChoicePreference(
|
||||
title = stringResource(pref.title),
|
||||
summary = pref.summary(context, null),
|
||||
possibleValues = navDrawerPins.keys,
|
||||
selectedValues = selectedItems.toSet(),
|
||||
onValueChange = { newSelectedItems ->
|
||||
viewModel.updatePins(newSelectedItems)
|
||||
},
|
||||
) {
|
||||
Text(it.name(context))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = pref.getter.invoke(preferences)
|
||||
ComposablePreference(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
package com.github.damontecres.wholphin.ui.preferences
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.isPinned
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||
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.RememberTabManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
|
@ -15,5 +27,54 @@ class PreferencesViewModel
|
|||
val preferenceDataStore: DataStore<AppPreferences>,
|
||||
val navigationManager: NavigationManager,
|
||||
val rememberTabManager: RememberTabManager,
|
||||
val serverRepository: ServerRepository,
|
||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
val serverPreferencesDao: ServerPreferencesDao,
|
||||
) : ViewModel(),
|
||||
RememberTabManager by rememberTabManager
|
||||
RememberTabManager by rememberTabManager {
|
||||
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
||||
val navDrawerPins = MutableLiveData<Map<NavDrawerItem, Boolean>>(mapOf())
|
||||
|
||||
init {
|
||||
viewModelScope.launchIO {
|
||||
serverRepository.currentUser?.let { user ->
|
||||
allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems()
|
||||
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
|
||||
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
|
||||
this@PreferencesViewModel.navDrawerPins.setValueOnMain(navDrawerPins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePins(newSelectedItems: List<NavDrawerItem>) {
|
||||
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||
serverRepository.currentUser?.let { user ->
|
||||
val disabledItems =
|
||||
mutableListOf<NavDrawerItem>().apply {
|
||||
addAll(allNavDrawerItems)
|
||||
removeAll(newSelectedItems)
|
||||
}
|
||||
val enabledItems = newSelectedItems.toSet()
|
||||
val toSave =
|
||||
disabledItems.map {
|
||||
NavDrawerPinnedItem(
|
||||
user.rowId,
|
||||
it.id,
|
||||
NavPinType.UNPINNED,
|
||||
)
|
||||
} +
|
||||
enabledItems.map {
|
||||
NavDrawerPinnedItem(
|
||||
user.rowId,
|
||||
it.id,
|
||||
NavPinType.PINNED,
|
||||
)
|
||||
}
|
||||
serverPreferencesDao.saveNavDrawerPinnedItems(*toSave.toTypedArray())
|
||||
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
|
||||
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
|
||||
this@PreferencesViewModel.navDrawerPins.setValueOnMain(navDrawerPins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,6 @@
|
|||
<string name="fa_list_ul" translatable="false"></string>
|
||||
<string name="fa_file_video" translatable="false"></string>
|
||||
<string name="fa_volume_low" translatable="false"></string>
|
||||
<string name="fa_ellipsis" translatable="false"></string>
|
||||
<string name="fa_ellipsis_vertical" translatable="false"></string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -88,5 +88,8 @@
|
|||
<string name="remove_favorite">Unfavorite</string>
|
||||
<string name="add_favorite">Favorite</string>
|
||||
<string name="favorites">Favorites</string>
|
||||
<string name="nav_drawer_pins">Customize Navigation Drawer Items</string>
|
||||
<string name="profile_specific_settings">User Profile Settings</string>
|
||||
<string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue