mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +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.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
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.NavDrawerPinnedItem
|
||||||
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],
|
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class],
|
||||||
version = 4,
|
version = 5,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [AutoMigration(3, 4)],
|
autoMigrations = [
|
||||||
|
AutoMigration(3, 4),
|
||||||
|
AutoMigration(4, 5),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun serverDao(): JellyfinServerDao
|
abstract fun serverDao(): JellyfinServerDao
|
||||||
|
|
||||||
abstract fun itemPlaybackDao(): ItemPlaybackDao
|
abstract fun itemPlaybackDao(): ItemPlaybackDao
|
||||||
|
|
||||||
|
abstract fun serverPreferencesDao(): ServerPreferencesDao
|
||||||
}
|
}
|
||||||
|
|
||||||
class Converters {
|
class Converters {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ interface JellyfinServerDao {
|
||||||
fun updateUser(user: JellyfinUser): Int
|
fun updateUser(user: JellyfinUser): Int
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
fun addOrUpdateUser(user: JellyfinUser) {
|
fun addOrUpdateUser(user: JellyfinUser): JellyfinUser {
|
||||||
val result = addUser(user)
|
val result = addUser(user)
|
||||||
if (result == -1L) {
|
if (result == -1L) {
|
||||||
val toSave =
|
val toSave =
|
||||||
|
|
@ -92,7 +92,9 @@ interface JellyfinServerDao {
|
||||||
user
|
user
|
||||||
}
|
}
|
||||||
updateUser(toSave)
|
updateUser(toSave)
|
||||||
|
return toSave
|
||||||
}
|
}
|
||||||
|
return user.copy(rowId = result.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM users WHERE serverId = :serverId AND id = :userId")
|
@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 sysInfo by apiClient.systemApi.getSystemInfo()
|
||||||
|
|
||||||
val updatedServer = server.copy(name = sysInfo.serverName)
|
val updatedServer = server.copy(name = sysInfo.serverName)
|
||||||
val updatedUser =
|
var updatedUser =
|
||||||
user.copy(
|
user.copy(
|
||||||
id = userDto.id,
|
id = userDto.id,
|
||||||
name = userDto.name,
|
name = userDto.name,
|
||||||
)
|
)
|
||||||
if (updatedUser.rowId <= 0) {
|
|
||||||
}
|
|
||||||
serverDao.addOrUpdateServer(updatedServer)
|
serverDao.addOrUpdateServer(updatedServer)
|
||||||
serverDao.addOrUpdateUser(updatedUser)
|
updatedUser = serverDao.addOrUpdateUser(updatedUser)
|
||||||
userPreferencesDataStore.updateData {
|
userPreferencesDataStore.updateData {
|
||||||
it
|
it
|
||||||
.toBuilder()
|
.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.ItemPlaybackDao
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||||
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.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -46,6 +47,10 @@ object DatabaseModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun itemPlaybackDao(db: AppDatabase): ItemPlaybackDao = db.itemPlaybackDao()
|
fun itemPlaybackDao(db: AppDatabase): ItemPlaybackDao = db.itemPlaybackDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun serverPreferencesDao(db: AppDatabase): ServerPreferencesDao = db.serverPreferencesDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun userPreferencesDataStore(
|
fun userPreferencesDataStore(
|
||||||
|
|
|
||||||
|
|
@ -526,6 +526,14 @@ sealed interface AppPreference<T> {
|
||||||
getter = { },
|
getter = { },
|
||||||
setter = { prefs, _ -> prefs },
|
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,
|
AppPreference.SkipBackOnResume,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PreferenceGroup(
|
||||||
|
title = R.string.profile_specific_settings,
|
||||||
|
preferences =
|
||||||
|
listOf(
|
||||||
|
AppPreference.UserPinnedNavDrawerItems,
|
||||||
|
),
|
||||||
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
title = R.string.about,
|
title = R.string.about,
|
||||||
preferences =
|
preferences =
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@ package com.github.damontecres.wholphin.ui.main
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
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.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
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.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
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.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||||
import org.jellyfin.sdk.model.api.UserDto
|
import org.jellyfin.sdk.model.api.UserDto
|
||||||
|
|
@ -36,6 +38,8 @@ class HomeViewModel
|
||||||
constructor(
|
constructor(
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val homeRows = MutableLiveData<List<HomeRow>>()
|
val homeRows = MutableLiveData<List<HomeRow>>()
|
||||||
|
|
@ -52,64 +56,44 @@ class HomeViewModel
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Timber.d("init 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
|
serverRepository.currentUserDto?.let { userDto ->
|
||||||
// .filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() }
|
val includedIds =
|
||||||
// .sortedBy { it.key }
|
navDrawerItemRepository
|
||||||
// .map { HomeSection.fromKey(it.value ?: "") }
|
.getFilteredNavDrawerItems(navDrawerItemRepository.getNavDrawerItems())
|
||||||
// .filter {
|
.filter { it is ServerNavDrawerItem }
|
||||||
// it in
|
.map { (it as ServerNavDrawerItem).itemId }
|
||||||
// setOf(
|
// TODO data is fetched all together which may be slow for large servers
|
||||||
// HomeSection.LATEST_MEDIA,
|
val resume = getResume(userDto.id, limit)
|
||||||
// HomeSection.NEXT_UP,
|
val nextUp = getNextUp(userDto.id, limit, prefs.enableRewatchingNextUp)
|
||||||
// HomeSection.RESUME,
|
val latest = getLatest(userDto, limit, includedIds)
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO data is fetched all together which may be slow for large servers
|
val homeRows =
|
||||||
val resume = getResume(user.id, limit)
|
if (prefs.combineContinueNext) {
|
||||||
val nextUp = getNextUp(user.id, limit, prefs.enableRewatchingNextUp)
|
listOf(
|
||||||
val latest = getLatest(user, limit)
|
HomeRow(
|
||||||
|
section = HomeSection.NEXT_UP,
|
||||||
val homeRows =
|
items = resume + nextUp,
|
||||||
if (prefs.combineContinueNext) {
|
),
|
||||||
listOf(
|
*latest.toTypedArray(),
|
||||||
HomeRow(
|
)
|
||||||
section = HomeSection.NEXT_UP,
|
} else {
|
||||||
items = resume + nextUp,
|
listOf(
|
||||||
),
|
HomeRow(
|
||||||
*latest.toTypedArray(),
|
section = HomeSection.RESUME,
|
||||||
)
|
items = resume,
|
||||||
} else {
|
),
|
||||||
listOf(
|
HomeRow(
|
||||||
HomeRow(
|
section = HomeSection.NEXT_UP,
|
||||||
section = HomeSection.RESUME,
|
items = nextUp,
|
||||||
items = resume,
|
),
|
||||||
),
|
*latest.toTypedArray(),
|
||||||
HomeRow(
|
)
|
||||||
section = HomeSection.NEXT_UP,
|
}
|
||||||
items = nextUp,
|
withContext(Dispatchers.Main) {
|
||||||
),
|
this@HomeViewModel.homeRows.value = homeRows
|
||||||
*latest.toTypedArray(),
|
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(
|
private suspend fun getLatest(
|
||||||
user: UserDto,
|
user: UserDto,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
|
includedIds: List<UUID>,
|
||||||
): List<HomeRow> {
|
): List<HomeRow> {
|
||||||
val latestMediaIncludes =
|
val latestMediaIncludes =
|
||||||
user.configuration?.orderedViews.orEmpty().toMutableList().apply {
|
user.configuration
|
||||||
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
?.orderedViews
|
||||||
}
|
.orEmpty()
|
||||||
|
.toMutableList()
|
||||||
|
.apply {
|
||||||
|
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
||||||
|
}.filter { includedIds.contains(it) }
|
||||||
|
|
||||||
val views by api.userViewsApi.getUserViews()
|
val views by api.userViewsApi.getUserViews()
|
||||||
val rows =
|
val rows =
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
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.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
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.Home
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -49,6 +51,7 @@ import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.NavigationDrawer
|
import androidx.tv.material3.NavigationDrawer
|
||||||
import androidx.tv.material3.NavigationDrawerItem
|
import androidx.tv.material3.NavigationDrawerItem
|
||||||
|
import androidx.tv.material3.NavigationDrawerItemDefaults
|
||||||
import androidx.tv.material3.NavigationDrawerScope
|
import androidx.tv.material3.NavigationDrawerScope
|
||||||
import androidx.tv.material3.ProvideTextStyle
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
import androidx.tv.material3.Text
|
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.R
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServer
|
import com.github.damontecres.wholphin.data.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.JellyfinUser
|
import com.github.damontecres.wholphin.data.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
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.preferences.PreferenceScreenOption
|
||||||
import com.github.damontecres.wholphin.ui.spacedByWithFooter
|
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.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.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -84,45 +81,25 @@ import javax.inject.Inject
|
||||||
class NavDrawerViewModel
|
class NavDrawerViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@param:ApplicationContext val context: Context,
|
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
val serverRepository: ServerRepository,
|
|
||||||
val api: ApiClient,
|
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private var all: List<NavDrawerItem>? = null
|
||||||
|
val moreLibraries = MutableLiveData<List<NavDrawerItem>>(null)
|
||||||
val libraries = MutableLiveData<List<NavDrawerItem>>(listOf())
|
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) {
|
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) {
|
fun setIndex(index: Int) {
|
||||||
selectedIndex.value = index
|
selectedIndex.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setShowMore(value: Boolean) {
|
||||||
|
showMore.value = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class NavDrawerItem(
|
sealed interface NavDrawerItem {
|
||||||
val id: UUID,
|
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 name: String,
|
||||||
val destination: Destination,
|
val destination: Destination,
|
||||||
val type: CollectionType,
|
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
|
* Display the left side navigation drawer with [DestinationContent] on the right
|
||||||
|
|
@ -162,17 +166,49 @@ fun NavDrawer(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val drawerFocusRequester = remember { FocusRequester() }
|
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
|
// 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)) {
|
BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination is Destination.Home)) {
|
||||||
drawerState.setValue(DrawerValue.Open)
|
drawerState.setValue(DrawerValue.Open)
|
||||||
drawerFocusRequester.requestFocus()
|
drawerFocusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
val moreLibraries by viewModel.moreLibraries.observeAsState(listOf())
|
||||||
val libraries by viewModel.libraries.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
|
// A negative index is a built in page, >=0 is a library
|
||||||
val selectedIndex by viewModel.selectedIndex.observeAsState(-1)
|
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(
|
NavigationDrawer(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -205,6 +241,7 @@ fun NavDrawer(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.navigationManager.navigateTo(Destination.UserList)
|
viewModel.navigationManager.navigateTo(Destination.UserList)
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -217,10 +254,11 @@ fun NavDrawer(
|
||||||
viewModel.navigationManager.navigateToFromDrawer(Destination.Search)
|
viewModel.navigationManager.navigateToFromDrawer(Destination.Search)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.ifElse(
|
Modifier
|
||||||
selectedIndex == -2,
|
.ifElse(
|
||||||
Modifier.focusRequester(focusRequester),
|
selectedIndex == -2,
|
||||||
),
|
Modifier.focusRequester(focusRequester),
|
||||||
|
).animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -237,27 +275,44 @@ fun NavDrawer(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.ifElse(
|
Modifier
|
||||||
selectedIndex == -1,
|
.ifElse(
|
||||||
Modifier.focusRequester(focusRequester),
|
selectedIndex == -1,
|
||||||
),
|
Modifier.focusRequester(focusRequester),
|
||||||
|
).animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemsIndexed(libraries) { index, it ->
|
itemsIndexed(libraries) { index, it ->
|
||||||
NavItem(
|
NavItem(
|
||||||
library = it,
|
library = it,
|
||||||
selected = selectedIndex == index,
|
selected = selectedIndex == index,
|
||||||
onClick = {
|
moreExpanded = showMore,
|
||||||
viewModel.setIndex(index)
|
onClick = { onClick.invoke(index, it) },
|
||||||
viewModel.navigationManager.navigateToFromDrawer(it.destination)
|
|
||||||
},
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.ifElse(
|
Modifier
|
||||||
selectedIndex == index,
|
.ifElse(
|
||||||
Modifier.focusRequester(focusRequester),
|
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 {
|
item {
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
text = "Settings",
|
text = "Settings",
|
||||||
|
|
@ -270,7 +325,7 @@ fun NavDrawer(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = Modifier.animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -339,20 +394,29 @@ fun NavigationDrawerScope.NavItem(
|
||||||
library: NavDrawerItem,
|
library: NavDrawerItem,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
|
moreExpanded: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
containerColor: Color = Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val icon =
|
val icon =
|
||||||
when (library.type) {
|
when (library) {
|
||||||
CollectionType.MOVIES -> R.string.fa_film
|
NavDrawerItem.Favorites -> R.string.fa_heart
|
||||||
CollectionType.TVSHOWS -> R.string.fa_tv
|
NavDrawerItem.More -> R.string.fa_ellipsis
|
||||||
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
|
||||||
CollectionType.LIVETV -> R.string.fa_tv
|
is ServerNavDrawerItem ->
|
||||||
CollectionType.MUSIC -> R.string.fa_music
|
when (library.type) {
|
||||||
CollectionType.BOXSETS -> R.string.fa_open_folder
|
CollectionType.MOVIES -> R.string.fa_film
|
||||||
CollectionType.PLAYLISTS -> R.string.fa_list_ul
|
CollectionType.TVSHOWS -> R.string.fa_tv
|
||||||
else -> library.iconStringRes
|
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
||||||
} ?: R.string.fa_film
|
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 isFocused = interactionSource.collectIsFocusedAsState().value
|
||||||
val color =
|
val color =
|
||||||
when {
|
when {
|
||||||
|
|
@ -364,6 +428,10 @@ fun NavigationDrawerScope.NavItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
selected = false,
|
selected = false,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
colors =
|
||||||
|
NavigationDrawerItemDefaults.colors(
|
||||||
|
containerColor = containerColor,
|
||||||
|
),
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text(
|
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,
|
interactionSource = null,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
text = library.name ?: library.id.toString(),
|
text = library.name(context),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Switch
|
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
|
@ -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.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.SortedSet
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -197,7 +197,7 @@ fun <T> ComposablePreference(
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppMultiChoicePreference<*> -> {
|
is AppMultiChoicePreference<*> -> {
|
||||||
val values = stringArrayResource(preference.displayValues).toList()
|
val values = stringArrayResource(preference.displayValues).toSortedSet()
|
||||||
val summary =
|
val summary =
|
||||||
preference.summary?.let { stringResource(it) }
|
preference.summary?.let { stringResource(it) }
|
||||||
?: preference.summary(context, value)
|
?: preference.summary(context, value)
|
||||||
|
|
@ -207,46 +207,14 @@ fun <T> ComposablePreference(
|
||||||
list.addAll(value as List<Any>)
|
list.addAll(value as List<Any>)
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
|
MultiChoicePreference(
|
||||||
val onClick = { item: Any ->
|
possibleValues = values as SortedSet<Any>,
|
||||||
if (selectedValues.contains(item)) {
|
selectedValues = selectedValues,
|
||||||
selectedValues.remove(item)
|
|
||||||
} else {
|
|
||||||
selectedValues.add(item)
|
|
||||||
}
|
|
||||||
onValueChange.invoke(selectedValues.toList() as T)
|
|
||||||
}
|
|
||||||
|
|
||||||
ClickPreference(
|
|
||||||
title = title,
|
title = title,
|
||||||
summary = summary,
|
summary = summary,
|
||||||
onClick = {
|
onValueChange = {
|
||||||
dialogParams =
|
onValueChange.invoke(selectedValues.toList() as T)
|
||||||
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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
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)) }
|
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
var preferences by remember { mutableStateOf(initialPreferences) }
|
var preferences by remember { mutableStateOf(initialPreferences) }
|
||||||
|
|
||||||
|
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.preferenceDataStore.data.collect {
|
viewModel.preferenceDataStore.data.collect {
|
||||||
preferences = it
|
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 -> {
|
else -> {
|
||||||
val value = pref.getter.invoke(preferences)
|
val value = pref.getter.invoke(preferences)
|
||||||
ComposablePreference(
|
ComposablePreference(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
package com.github.damontecres.wholphin.ui.preferences
|
package com.github.damontecres.wholphin.ui.preferences
|
||||||
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
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.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.nav.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.RememberTabManager
|
import com.github.damontecres.wholphin.util.RememberTabManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -15,5 +27,54 @@ class PreferencesViewModel
|
||||||
val preferenceDataStore: DataStore<AppPreferences>,
|
val preferenceDataStore: DataStore<AppPreferences>,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
val rememberTabManager: RememberTabManager,
|
val rememberTabManager: RememberTabManager,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
|
val serverPreferencesDao: ServerPreferencesDao,
|
||||||
) : ViewModel(),
|
) : 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_list_ul" translatable="false"></string>
|
||||||
<string name="fa_file_video" translatable="false"></string>
|
<string name="fa_file_video" translatable="false"></string>
|
||||||
<string name="fa_volume_low" 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>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -88,5 +88,8 @@
|
||||||
<string name="remove_favorite">Unfavorite</string>
|
<string name="remove_favorite">Unfavorite</string>
|
||||||
<string name="add_favorite">Favorite</string>
|
<string name="add_favorite">Favorite</string>
|
||||||
<string name="favorites">Favorites</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>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue