mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add ability to order navigation drawer items (#886)
## Description Allows for reordering the items in the navigation drawer This does not change the home page row order since #803 decouples the nav drawer and home page rows. The initial order will be the "Library Order" settings on the web under Profile->Home. Making any changes locally in Wholphin will set the order. If any new libraries are added or if Seerr integration is enabled, these will added to the end of the "pinned" list. ### Related issues Closes #822 Related to #399 & #803 ### Testing Tested on emulator - Re-ordering - Granting/Revoking user permission to a new library ## Screenshots N/A, nav drawer is the same, just sorted differently ## AI or LLM usage None
This commit is contained in:
parent
fcba1c7444
commit
17b0eef5c5
9 changed files with 1214 additions and 163 deletions
|
|
@ -0,0 +1,642 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 31,
|
||||||
|
"identityHash": "c6829d764ec85321ab3be9905d6c0e3a",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, `pin` 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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pin",
|
||||||
|
"columnName": "pin",
|
||||||
|
"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, `order` INTEGER NOT NULL DEFAULT -1, 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "LibraryDisplayInfo",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sort",
|
||||||
|
"columnName": "sort",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "direction",
|
||||||
|
"columnName": "direction",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "filter",
|
||||||
|
"columnName": "filter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "'{}'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewOptions",
|
||||||
|
"columnName": "viewOptions",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_LibraryDisplayInfo_userId_itemId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playback_effects",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "jellyfinUserRowId",
|
||||||
|
"columnName": "jellyfinUserRowId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.rotation",
|
||||||
|
"columnName": "rotation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.brightness",
|
||||||
|
"columnName": "brightness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.contrast",
|
||||||
|
"columnName": "contrast",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.saturation",
|
||||||
|
"columnName": "saturation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.hue",
|
||||||
|
"columnName": "hue",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.red",
|
||||||
|
"columnName": "red",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.green",
|
||||||
|
"columnName": "green",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.blue",
|
||||||
|
"columnName": "blue",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.blur",
|
||||||
|
"columnName": "blur",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"jellyfinUserRowId",
|
||||||
|
"itemId",
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "PlaybackLanguageChoice",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesId",
|
||||||
|
"columnName": "seriesId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "audioLanguage",
|
||||||
|
"columnName": "audioLanguage",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitleLanguage",
|
||||||
|
"columnName": "subtitleLanguage",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitlesDisabled",
|
||||||
|
"columnName": "subtitlesDisabled",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"seriesId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ItemTrackModification",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), 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": "trackIndex",
|
||||||
|
"columnName": "trackIndex",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "delayMs",
|
||||||
|
"columnName": "delayMs",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId",
|
||||||
|
"trackIndex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "seerr_servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_seerr_servers_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "seerr_users",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "jellyfinUserRowId",
|
||||||
|
"columnName": "jellyfinUserRowId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authMethod",
|
||||||
|
"columnName": "authMethod",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "credential",
|
||||||
|
"columnName": "credential",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"jellyfinUserRowId",
|
||||||
|
"serverId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "seerr_servers",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"jellyfinUserRowId"
|
||||||
|
],
|
||||||
|
"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, 'c6829d764ec85321ab3be9905d6c0e3a')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ import java.util.UUID
|
||||||
SeerrUser::class,
|
SeerrUser::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
version = 30,
|
version = 31,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
|
|
@ -54,6 +54,7 @@ import java.util.UUID
|
||||||
AutoMigration(11, 12),
|
AutoMigration(11, 12),
|
||||||
AutoMigration(12, 20),
|
AutoMigration(12, 20),
|
||||||
AutoMigration(20, 30),
|
AutoMigration(20, 30),
|
||||||
|
AutoMigration(30, 31),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.data.model
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
|
|
||||||
|
|
@ -24,4 +25,5 @@ data class NavDrawerPinnedItem(
|
||||||
val userId: Int,
|
val userId: Int,
|
||||||
val itemId: String,
|
val itemId: String,
|
||||||
val type: NavPinType,
|
val type: NavPinType,
|
||||||
|
@ColumnInfo(defaultValue = "-1") val order: Int,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
|
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||||
|
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
|
import org.jellyfin.sdk.model.api.UserDto
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class NavDrawerService
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
@param:DefaultCoroutineScope private val coroutineScope: CoroutineScope,
|
||||||
|
private val api: ApiClient,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
|
private val serverPreferencesDao: ServerPreferencesDao,
|
||||||
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
|
) {
|
||||||
|
private val _state = MutableStateFlow(NavDrawerItemState.EMPTY)
|
||||||
|
val state: StateFlow<NavDrawerItemState> = _state
|
||||||
|
|
||||||
|
init {
|
||||||
|
serverRepository.currentUser
|
||||||
|
.asFlow()
|
||||||
|
.combine(serverRepository.currentUserDto.asFlow()) { user, userDto ->
|
||||||
|
Pair(user, userDto)
|
||||||
|
}.onEach { (user, userDto) ->
|
||||||
|
Timber.d("User updated: user=%s, userDto=%s", user?.id, userDto?.id)
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
items = emptyList(),
|
||||||
|
moreItems = emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (user != null && userDto != null && user.id == userDto.id) {
|
||||||
|
updateNavDrawer(user, userDto)
|
||||||
|
}
|
||||||
|
}.launchIn(coroutineScope)
|
||||||
|
seerrServerRepository.active
|
||||||
|
.onEach { discoverActive ->
|
||||||
|
_state.update { it.copy(discoverEnabled = discoverActive) }
|
||||||
|
}.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateNavDrawer(
|
||||||
|
user: JellyfinUser,
|
||||||
|
userDto: UserDto,
|
||||||
|
) {
|
||||||
|
val tvAccess = userDto.policy?.enableLiveTvAccess ?: false
|
||||||
|
val userViews =
|
||||||
|
api.userViewsApi
|
||||||
|
.getUserViews(userId = user.id)
|
||||||
|
.content.items
|
||||||
|
val recordingFolders =
|
||||||
|
if (tvAccess) {
|
||||||
|
api.liveTvApi
|
||||||
|
.getRecordingFolders(userId = user.id)
|
||||||
|
.content.items
|
||||||
|
.map { it.id }
|
||||||
|
.toSet()
|
||||||
|
} else {
|
||||||
|
setOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
val builtins = listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
|
||||||
|
|
||||||
|
val libraries =
|
||||||
|
userViews
|
||||||
|
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
|
||||||
|
.map {
|
||||||
|
val destination =
|
||||||
|
if (it.id in recordingFolders) {
|
||||||
|
Destination.Recordings(it.id)
|
||||||
|
} else {
|
||||||
|
BaseItem.from(it, api).destination()
|
||||||
|
}
|
||||||
|
ServerNavDrawerItem(
|
||||||
|
itemId = it.id,
|
||||||
|
name = it.name ?: it.id.toString(),
|
||||||
|
destination = destination,
|
||||||
|
type = it.collectionType ?: CollectionType.UNKNOWN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val allItems = builtins + libraries
|
||||||
|
|
||||||
|
val navDrawerPins =
|
||||||
|
serverPreferencesDao.getNavDrawerPinnedItems(user).associateBy { it.itemId }
|
||||||
|
|
||||||
|
val items = mutableListOf<NavDrawerItem>()
|
||||||
|
val moreItems = mutableListOf<NavDrawerItem>()
|
||||||
|
allItems
|
||||||
|
// Sort by order if non-default, existing items before customize will have -1 value
|
||||||
|
// New items from the server will get Int.MAX_VALUE
|
||||||
|
// Items the user doesn't have access to anymore will be skipped
|
||||||
|
.sortedBy { navDrawerPins[it.id]?.order?.takeIf { it >= 0 } ?: Int.MAX_VALUE }
|
||||||
|
.forEach {
|
||||||
|
// Assume pinned if unknown
|
||||||
|
val pinned = navDrawerPins[it.id]?.type ?: NavPinType.PINNED
|
||||||
|
if (pinned == NavPinType.PINNED) {
|
||||||
|
items.add(it)
|
||||||
|
} else {
|
||||||
|
moreItems.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
items = items,
|
||||||
|
moreItems = moreItems,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NavDrawerItemState(
|
||||||
|
val items: List<NavDrawerItem>,
|
||||||
|
val moreItems: List<NavDrawerItem>,
|
||||||
|
val discoverEnabled: Boolean,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,10 @@ annotation class StandardOkHttpClient
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
annotation class IoCoroutineScope
|
annotation class IoCoroutineScope
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class DefaultCoroutineScope
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object AppModule {
|
object AppModule {
|
||||||
|
|
@ -177,6 +181,11 @@ object AppModule {
|
||||||
@IoCoroutineScope
|
@IoCoroutineScope
|
||||||
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@DefaultCoroutineScope
|
||||||
|
fun defaultCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun workManager(
|
fun workManager(
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ 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.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
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
|
||||||
|
|
@ -54,7 +54,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.tv.material3.DrawerState
|
import androidx.tv.material3.DrawerState
|
||||||
import androidx.tv.material3.DrawerValue
|
import androidx.tv.material3.DrawerValue
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
|
|
@ -72,6 +71,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
|
import com.github.damontecres.wholphin.services.NavDrawerService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.services.SetupDestination
|
import com.github.damontecres.wholphin.services.SetupDestination
|
||||||
|
|
@ -79,23 +79,16 @@ import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
||||||
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.setValueOnMain
|
|
||||||
import com.github.damontecres.wholphin.ui.setup.UserIconCardImage
|
import com.github.damontecres.wholphin.ui.setup.UserIconCardImage
|
||||||
import com.github.damontecres.wholphin.ui.spacedByWithFooter
|
import com.github.damontecres.wholphin.ui.spacedByWithFooter
|
||||||
import com.github.damontecres.wholphin.ui.theme.LocalTheme
|
import com.github.damontecres.wholphin.ui.theme.LocalTheme
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
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.imageApi
|
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -104,80 +97,17 @@ class NavDrawerViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
|
private val navDrawerService: NavDrawerService,
|
||||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
val setupNavigationManager: SetupNavigationManager,
|
val setupNavigationManager: SetupNavigationManager,
|
||||||
val backdropService: BackdropService,
|
val backdropService: BackdropService,
|
||||||
private val seerrServerRepository: SeerrServerRepository,
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val moreLibraries = MutableLiveData<List<NavDrawerItem>>(null)
|
val state = navDrawerService.state
|
||||||
val libraries = MutableLiveData<List<NavDrawerItem>>(listOf())
|
|
||||||
|
|
||||||
val selectedIndex = MutableLiveData(-1)
|
val selectedIndex = MutableLiveData(-1)
|
||||||
val showMore = MutableLiveData(false)
|
val moreExpanded = MutableLiveData(false)
|
||||||
|
|
||||||
init {
|
|
||||||
seerrServerRepository.active
|
|
||||||
.onEach {
|
|
||||||
init()
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init() {
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
val all = navDrawerItemRepository.getNavDrawerItems()
|
|
||||||
val libraries = navDrawerItemRepository.getFilteredNavDrawerItems(all)
|
|
||||||
val moreLibraries = all.toMutableList().apply { removeAll(libraries) }
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
this@NavDrawerViewModel.moreLibraries.value = moreLibraries
|
|
||||||
this@NavDrawerViewModel.libraries.value = libraries
|
|
||||||
}
|
|
||||||
val asDestinations =
|
|
||||||
(
|
|
||||||
libraries +
|
|
||||||
listOf(
|
|
||||||
NavDrawerItem.More,
|
|
||||||
NavDrawerItem.Discover,
|
|
||||||
) + moreLibraries
|
|
||||||
).map {
|
|
||||||
if (it is ServerNavDrawerItem) {
|
|
||||||
it.destination
|
|
||||||
} else if (it is NavDrawerItem.Favorites) {
|
|
||||||
Destination.Favorites
|
|
||||||
} else if (it is NavDrawerItem.Discover) {
|
|
||||||
Destination.Discover
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val backstack = navigationManager.backStack.toList().reversed()
|
|
||||||
for (i in 0..<backstack.size) {
|
|
||||||
val key = backstack[i]
|
|
||||||
if (key is Destination) {
|
|
||||||
val index =
|
|
||||||
if (key is Destination.Home) {
|
|
||||||
-1
|
|
||||||
} else if (key is Destination.Search) {
|
|
||||||
-2
|
|
||||||
} else {
|
|
||||||
val idx = asDestinations.indexOf(key)
|
|
||||||
if (idx >= 0) {
|
|
||||||
idx
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.v("Found $index => $key")
|
|
||||||
if (index != null) {
|
|
||||||
selectedIndex.setValueOnMain(index)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onClickDrawerItem(
|
fun onClickDrawerItem(
|
||||||
index: Int,
|
index: Int,
|
||||||
|
|
@ -193,7 +123,7 @@ class NavDrawerViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
NavDrawerItem.More -> {
|
NavDrawerItem.More -> {
|
||||||
setShowMore(!showMore.value!!)
|
setShowMore(!moreExpanded.value!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
NavDrawerItem.Discover -> {
|
NavDrawerItem.Discover -> {
|
||||||
|
|
@ -215,7 +145,7 @@ class NavDrawerViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShowMore(value: Boolean) {
|
fun setShowMore(value: Boolean) {
|
||||||
showMore.value = value
|
moreExpanded.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUserImage(user: JellyfinUser): String = api.imageApi.getUserImageUrl(user.id)
|
fun getUserImage(user: JellyfinUser): String = api.imageApi.getUserImageUrl(user.id)
|
||||||
|
|
@ -289,15 +219,12 @@ fun NavDrawer(
|
||||||
drawerState.setValue(DrawerValue.Open)
|
drawerState.setValue(DrawerValue.Open)
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
val moreLibraries by viewModel.moreLibraries.observeAsState(listOf())
|
val state by viewModel.state.collectAsState()
|
||||||
val libraries by viewModel.libraries.observeAsState(listOf())
|
val moreExpanded by viewModel.moreExpanded.observeAsState(false)
|
||||||
LaunchedEffect(Unit) { viewModel.init() }
|
// A negative index is a built-in page, >=0 is a library
|
||||||
|
|
||||||
val showMore by viewModel.showMore.observeAsState(false)
|
|
||||||
// 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)
|
||||||
|
|
||||||
BackHandler(enabled = showMore && drawerState.currentValue == DrawerValue.Open) {
|
BackHandler(enabled = moreExpanded && drawerState.currentValue == DrawerValue.Open) {
|
||||||
viewModel.setShowMore(false)
|
viewModel.setShowMore(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,51 +339,83 @@ fun NavDrawer(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemsIndexed(libraries) { index, it ->
|
itemsIndexed(state.items) { index, it ->
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
if (it !is NavDrawerItem.Discover || state.discoverEnabled) {
|
||||||
NavItem(
|
|
||||||
library = it,
|
|
||||||
selected = selectedIndex == index,
|
|
||||||
moreExpanded = showMore,
|
|
||||||
drawerOpen = isOpen,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = {
|
|
||||||
viewModel.onClickDrawerItem(index, it)
|
|
||||||
},
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.ifElse(
|
|
||||||
selectedIndex == index,
|
|
||||||
Modifier.focusRequester(focusRequester),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (showMore) {
|
|
||||||
itemsIndexed(moreLibraries) { index, it ->
|
|
||||||
val adjustedIndex = (index + libraries.size + 1)
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
NavItem(
|
NavItem(
|
||||||
library = it,
|
library = it,
|
||||||
selected = selectedIndex == adjustedIndex,
|
selected = selectedIndex == index,
|
||||||
moreExpanded = showMore,
|
moreExpanded = moreExpanded,
|
||||||
drawerOpen = isOpen,
|
drawerOpen = isOpen,
|
||||||
onClick = { viewModel.onClickDrawerItem(adjustedIndex, it) },
|
|
||||||
containerColor =
|
|
||||||
if (isOpen) {
|
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
|
||||||
} else {
|
|
||||||
Color.Unspecified
|
|
||||||
},
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
onClick = {
|
||||||
|
viewModel.onClickDrawerItem(index, it)
|
||||||
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.ifElse(
|
.ifElse(
|
||||||
selectedIndex == adjustedIndex,
|
selectedIndex == index,
|
||||||
Modifier.focusRequester(focusRequester),
|
Modifier.focusRequester(focusRequester),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (state.moreItems.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
val index = state.items.size
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
NavItem(
|
||||||
|
library = NavDrawerItem.More,
|
||||||
|
selected = selectedIndex == index,
|
||||||
|
moreExpanded = moreExpanded,
|
||||||
|
drawerOpen = isOpen,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = {
|
||||||
|
viewModel.onClickDrawerItem(index, NavDrawerItem.More)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.ifElse(
|
||||||
|
selectedIndex == index,
|
||||||
|
Modifier.focusRequester(focusRequester),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (moreExpanded) {
|
||||||
|
itemsIndexed(state.moreItems) { index, it ->
|
||||||
|
val adjustedIndex =
|
||||||
|
remember(state) { (index + state.items.size + 1) }
|
||||||
|
if (it !is NavDrawerItem.Discover || state.discoverEnabled) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
NavItem(
|
||||||
|
library = it,
|
||||||
|
selected = selectedIndex == adjustedIndex,
|
||||||
|
moreExpanded = moreExpanded,
|
||||||
|
drawerOpen = isOpen,
|
||||||
|
onClick = {
|
||||||
|
viewModel.onClickDrawerItem(
|
||||||
|
adjustedIndex,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor =
|
||||||
|
if (isOpen) {
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.ifElse(
|
||||||
|
selectedIndex == adjustedIndex,
|
||||||
|
Modifier.focusRequester(focusRequester),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Switch
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Button
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||||
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
|
||||||
|
data class NavDrawerPin(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val pinned: Boolean,
|
||||||
|
val item: NavDrawerItem,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(
|
||||||
|
context: Context,
|
||||||
|
items: Map<NavDrawerItem, Boolean>,
|
||||||
|
) {
|
||||||
|
items.map { (item, pinned) ->
|
||||||
|
NavDrawerPin(item.id, item.name(context), pinned, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class MoveDirection {
|
||||||
|
UP,
|
||||||
|
DOWN,
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> List<T>.move(
|
||||||
|
direction: MoveDirection,
|
||||||
|
index: Int,
|
||||||
|
): List<T> =
|
||||||
|
toMutableList().apply {
|
||||||
|
if (direction == MoveDirection.DOWN) {
|
||||||
|
val down = this[index]
|
||||||
|
val up = this[index + 1]
|
||||||
|
set(index, up)
|
||||||
|
set(index + 1, down)
|
||||||
|
} else {
|
||||||
|
val up = this[index]
|
||||||
|
val down = this[index - 1]
|
||||||
|
set(index - 1, up)
|
||||||
|
set(index, down)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavDrawerPreference(
|
||||||
|
title: String,
|
||||||
|
summary: String?,
|
||||||
|
items: List<NavDrawerPin>,
|
||||||
|
onSave: (List<NavDrawerPin>) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
ClickPreference(
|
||||||
|
title = title,
|
||||||
|
summary = summary,
|
||||||
|
onClick = { showDialog = true },
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
if (showDialog) {
|
||||||
|
NavDrawerPreferenceDialog(
|
||||||
|
items = items,
|
||||||
|
onDismissRequest = { showDialog = false },
|
||||||
|
onClick = { index ->
|
||||||
|
val newItems =
|
||||||
|
items.toMutableList().apply {
|
||||||
|
set(index, items[index].let { it.copy(pinned = !it.pinned) })
|
||||||
|
}
|
||||||
|
onSave.invoke(newItems)
|
||||||
|
},
|
||||||
|
onMoveUp = { index ->
|
||||||
|
onSave(items.move(MoveDirection.UP, index))
|
||||||
|
},
|
||||||
|
onMoveDown = { index ->
|
||||||
|
onSave(items.move(MoveDirection.DOWN, index))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavDrawerPreferenceDialog(
|
||||||
|
items: List<NavDrawerPin>,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onClick: (Int) -> Unit,
|
||||||
|
onMoveUp: (Int) -> Unit,
|
||||||
|
onMoveDown: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.nav_drawer_pins),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
) {
|
||||||
|
itemsIndexed(items, key = { _, item -> item.id }) { index, item ->
|
||||||
|
NavDrawerPreferenceListItem(
|
||||||
|
title = item.title,
|
||||||
|
pinned = item.pinned,
|
||||||
|
moveUpAllowed = index > 0,
|
||||||
|
moveDownAllowed = index < items.lastIndex,
|
||||||
|
onClick = { onClick.invoke(index) },
|
||||||
|
onMoveUp = { onMoveUp.invoke(index) },
|
||||||
|
onMoveDown = { onMoveDown.invoke(index) },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavDrawerPreferenceListItem(
|
||||||
|
title: String,
|
||||||
|
pinned: Boolean,
|
||||||
|
moveUpAllowed: Boolean,
|
||||||
|
moveDownAllowed: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onMoveUp: () -> Unit,
|
||||||
|
onMoveDown: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 40.dp, max = 88.dp),
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
selected = false,
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = pinned,
|
||||||
|
onCheckedChange = {
|
||||||
|
onClick.invoke()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.wrapContentWidth(),
|
||||||
|
) {
|
||||||
|
MoveButton(R.string.fa_caret_up, moveUpAllowed, onMoveUp)
|
||||||
|
MoveButton(R.string.fa_caret_down, moveDownAllowed, onMoveDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MoveButton(
|
||||||
|
@StringRes icon: Int,
|
||||||
|
enabled: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) = Button(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(icon),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontAwesome,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewTvSpec
|
||||||
|
@Composable
|
||||||
|
fun NavDrawerPreferenceListItemPreview() {
|
||||||
|
WholphinTheme {
|
||||||
|
NavDrawerPreferenceListItem(
|
||||||
|
title = "Movies",
|
||||||
|
pinned = true,
|
||||||
|
moveUpAllowed = true,
|
||||||
|
moveDownAllowed = true,
|
||||||
|
onClick = {},
|
||||||
|
onMoveUp = {},
|
||||||
|
onMoveDown = { },
|
||||||
|
modifier = Modifier.width(360.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,7 @@ fun PreferencesContent(
|
||||||
val currentServer by seerrVm.currentSeerrServer.collectAsState(null)
|
val currentServer by seerrVm.currentSeerrServer.collectAsState(null)
|
||||||
var showPinFlow by remember { mutableStateOf(false) }
|
var showPinFlow by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
val navDrawerPins by viewModel.navDrawerPins.collectAsState(emptyList())
|
||||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
||||||
val seerrIntegrationEnabled by viewModel.seerrEnabled.collectAsState(false)
|
val seerrIntegrationEnabled by viewModel.seerrEnabled.collectAsState(false)
|
||||||
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
|
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
|
||||||
|
|
@ -335,21 +335,16 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
AppPreference.UserPinnedNavDrawerItems -> {
|
AppPreference.UserPinnedNavDrawerItems -> {
|
||||||
val selectedItems =
|
NavDrawerPreference(
|
||||||
navDrawerPins.keys.mapNotNull {
|
|
||||||
if (navDrawerPins[it] ?: false) it else null
|
|
||||||
}
|
|
||||||
MultiChoicePreference(
|
|
||||||
title = stringResource(pref.title),
|
title = stringResource(pref.title),
|
||||||
summary = pref.summary(context, null),
|
summary = pref.summary(context, null),
|
||||||
possibleValues = navDrawerPins.keys,
|
items = navDrawerPins,
|
||||||
selectedValues = selectedItems.toSet(),
|
onSave = {
|
||||||
onValueChange = { newSelectedItems ->
|
viewModel.updatePins(it)
|
||||||
viewModel.updatePins(newSelectedItems)
|
|
||||||
},
|
},
|
||||||
) {
|
modifier = Modifier,
|
||||||
Text(it.name(context))
|
interactionSource = interactionSource,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppPreference.SendAppLogs -> {
|
AppPreference.SendAppLogs -> {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,12 @@ package com.github.damontecres.wholphin.ui.preferences
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
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.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.isPinned
|
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||||
|
|
@ -17,23 +15,28 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.resetSubtitles
|
import com.github.damontecres.wholphin.preferences.resetSubtitles
|
||||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
|
import com.github.damontecres.wholphin.services.NavDrawerService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
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 dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.model.ClientInfo
|
import org.jellyfin.sdk.model.ClientInfo
|
||||||
import org.jellyfin.sdk.model.DeviceInfo
|
import org.jellyfin.sdk.model.DeviceInfo
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -48,6 +51,7 @@ class PreferencesViewModel
|
||||||
private val rememberTabManager: RememberTabManager,
|
private val rememberTabManager: RememberTabManager,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
|
private val navDrawerService: NavDrawerService,
|
||||||
private val serverPreferencesDao: ServerPreferencesDao,
|
private val serverPreferencesDao: ServerPreferencesDao,
|
||||||
private val seerrServerRepository: SeerrServerRepository,
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
private val deviceInfo: DeviceInfo,
|
private val deviceInfo: DeviceInfo,
|
||||||
|
|
@ -55,7 +59,46 @@ class PreferencesViewModel
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
RememberTabManager by rememberTabManager {
|
RememberTabManager by rememberTabManager {
|
||||||
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
||||||
val navDrawerPins = MutableLiveData<Map<NavDrawerItem, Boolean>>(mapOf())
|
// val navDrawerPins = MutableLiveData<List<NavDrawerPin>>(emptyList())
|
||||||
|
|
||||||
|
val navDrawerPins =
|
||||||
|
navDrawerService.state
|
||||||
|
.combine(
|
||||||
|
serverRepository.currentUser.asFlow(),
|
||||||
|
) { state, user ->
|
||||||
|
Pair(state, user)
|
||||||
|
}.combine(seerrServerRepository.active) { (state, user), seerr ->
|
||||||
|
Triple(state, user, seerr)
|
||||||
|
}.map { (state, user, seerr) ->
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val navDrawerPins =
|
||||||
|
serverPreferencesDao
|
||||||
|
.getNavDrawerPinnedItems(user!!)
|
||||||
|
.associateBy { it.itemId }
|
||||||
|
|
||||||
|
val allItems = state.let { it.items + it.moreItems }
|
||||||
|
val pins =
|
||||||
|
allItems
|
||||||
|
.sortedBy {
|
||||||
|
navDrawerPins[it.id]?.order?.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
||||||
|
}.mapNotNull {
|
||||||
|
if (!seerr && it is NavDrawerItem.Discover) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
// Assume pinned if unknown
|
||||||
|
val pinned = navDrawerPins[it.id]?.type ?: NavPinType.PINNED
|
||||||
|
NavDrawerPin(
|
||||||
|
it.id,
|
||||||
|
it.name(context),
|
||||||
|
pinned == NavPinType.PINNED,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val currentUser get() = serverRepository.currentUser
|
val currentUser get() = serverRepository.currentUser
|
||||||
|
|
||||||
|
|
@ -70,42 +113,50 @@ class PreferencesViewModel
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
serverRepository.currentUser.value?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems()
|
// fetchNavDrawerPins(user)
|
||||||
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
|
|
||||||
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
|
|
||||||
this@PreferencesViewModel.navDrawerPins.setValueOnMain(navDrawerPins)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePins(newSelectedItems: List<NavDrawerItem>) {
|
private suspend fun fetchNavDrawerPins(user: JellyfinUser) {
|
||||||
|
navDrawerService.state.map {
|
||||||
|
val navDrawerPins =
|
||||||
|
serverPreferencesDao.getNavDrawerPinnedItems(user).associateBy { it.itemId }
|
||||||
|
|
||||||
|
val allItems = navDrawerService.state.first().let { it.items + it.moreItems }
|
||||||
|
val pins =
|
||||||
|
allItems
|
||||||
|
.sortedBy { navDrawerPins[it.id]?.order?.takeIf { it >= 0 } ?: Int.MAX_VALUE }
|
||||||
|
.map {
|
||||||
|
// Assume pinned if unknown
|
||||||
|
val pinned = navDrawerPins[it.id]?.type ?: NavPinType.PINNED
|
||||||
|
NavDrawerPin(it.id, it.name(context), pinned == NavPinType.PINNED, it)
|
||||||
|
}
|
||||||
|
pins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePins(items: List<NavDrawerPin>) {
|
||||||
viewModelScope.launchIO(ExceptionHandler(true)) {
|
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||||
serverRepository.currentUser.value?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
val disabledItems =
|
serverRepository.currentUserDto.value?.let { userDto ->
|
||||||
mutableListOf<NavDrawerItem>().apply {
|
if (user.id == userDto.id) {
|
||||||
addAll(allNavDrawerItems)
|
Timber.v("Updating pins")
|
||||||
removeAll(newSelectedItems)
|
val toSave =
|
||||||
|
items.mapIndexed { index, item ->
|
||||||
|
NavDrawerPinnedItem(
|
||||||
|
user.rowId,
|
||||||
|
item.id,
|
||||||
|
if (item.pinned) NavPinType.PINNED else NavPinType.UNPINNED,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
serverPreferencesDao.saveNavDrawerPinnedItems(*toSave.toTypedArray())
|
||||||
|
navDrawerService.updateNavDrawer(user, userDto)
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("User IDs do not match")
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue