mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Support configuring interface language per user (#1406)
## Description Adds a setting to configure the app's language/locale. It is configured per user. This settings has no impact on audio or subtitles language choices. Only languages Wholphin has translations for can be used. If you would like to contribute new translations, check out https://translate.codeberg.org/projects/wholphin/! Dev notes: This doesn't exactly follow Android's recommendations because the [guidelines are for per-app](https://developer.android.com/guide/topics/resources/app-languages) and not users within the app. ### Related issues Closes #513 Closes #669 ### Testing - Emulator API 34 - Nvidia shield API 30 - Fire TV OS 7 API 28 ## Screenshots <img width="1280" height="720" alt="language Large" src="https://github.com/user-attachments/assets/bf1c0962-8a36-4012-aed9-eab564fc1114" /> ## AI or LLM usage None
This commit is contained in:
parent
625cdcb4a3
commit
903c45dfb8
43 changed files with 1501 additions and 418 deletions
|
|
@ -184,6 +184,9 @@ configure<ApplicationExtension> {
|
||||||
lint {
|
lint {
|
||||||
disable.add("MissingTranslation")
|
disable.add("MissingTranslation")
|
||||||
}
|
}
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,664 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 33,
|
||||||
|
"identityHash": "a65eec85c027fac13948a0dd6f99d3cb",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` 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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "lastUsed",
|
||||||
|
"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, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` 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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "requireLogin",
|
||||||
|
"columnName": "requireLogin",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "lastUsed",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uiLanguage",
|
||||||
|
"columnName": "uiLanguage",
|
||||||
|
"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, 'a65eec85c027fac13948a0dd6f99d3cb')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -84,6 +84,15 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data
|
||||||
|
android:name="autoStoreLocales"
|
||||||
|
android:value="false" />
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,8 @@ import java.util.UUID
|
||||||
ItemTrackModification::class,
|
ItemTrackModification::class,
|
||||||
SeerrServer::class,
|
SeerrServer::class,
|
||||||
SeerrUser::class,
|
SeerrUser::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
version = 32,
|
version = 33,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
|
|
@ -64,6 +63,7 @@ import java.util.UUID
|
||||||
AutoMigration(20, 30),
|
AutoMigration(20, 30),
|
||||||
AutoMigration(30, 31),
|
AutoMigration(30, 31),
|
||||||
AutoMigration(31, 32),
|
AutoMigration(31, 32),
|
||||||
|
AutoMigration(32, 33),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.AspectRatio
|
import com.github.damontecres.wholphin.ui.AspectRatio
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
import org.jellyfin.sdk.model.api.ExtraType
|
import org.jellyfin.sdk.model.api.ExtraType
|
||||||
|
|
@ -41,8 +42,7 @@ sealed interface ExtrasItem {
|
||||||
) : ExtrasItem {
|
) : ExtrasItem {
|
||||||
override val destination: Destination =
|
override val destination: Destination =
|
||||||
Destination.ItemGrid(
|
Destination.ItemGrid(
|
||||||
title = null,
|
title = ResStringProvider(type.stringRes),
|
||||||
titleRes = type.stringRes,
|
|
||||||
request =
|
request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
ids = items.map { it.id },
|
ids = items.map { it.id },
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ data class JellyfinUser(
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo(defaultValue = "false")
|
||||||
val requireLogin: Boolean = false,
|
val requireLogin: Boolean = false,
|
||||||
val lastUsed: ZonedDateTime? = null,
|
val lastUsed: ZonedDateTime? = null,
|
||||||
|
val uiLanguage: String? = null,
|
||||||
) {
|
) {
|
||||||
val hasPin: Boolean get() = pin.isNotNullOrBlank()
|
val hasPin: Boolean get() = pin.isNotNullOrBlank()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -704,6 +704,13 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summary = R.string.customize_home_summary,
|
summary = R.string.customize_home_summary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val UserInterfaceLanguage =
|
||||||
|
AppClickablePreference<AppPreferences>(
|
||||||
|
title = R.string.user_interface_language,
|
||||||
|
getter = { },
|
||||||
|
setter = { prefs, _ -> prefs },
|
||||||
|
)
|
||||||
|
|
||||||
val SendCrashReports =
|
val SendCrashReports =
|
||||||
AppSwitchPreference<AppPreferences>(
|
AppSwitchPreference<AppPreferences>(
|
||||||
title = R.string.send_crash_reports,
|
title = R.string.send_crash_reports,
|
||||||
|
|
@ -1110,6 +1117,7 @@ val basicPreferences =
|
||||||
AppPreference.ProtectProfilePreference,
|
AppPreference.ProtectProfilePreference,
|
||||||
AppPreference.CustomizeHome,
|
AppPreference.CustomizeHome,
|
||||||
AppPreference.UserPinnedNavDrawerItems,
|
AppPreference.UserPinnedNavDrawerItems,
|
||||||
|
AppPreference.UserInterfaceLanguage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
|
@ -19,6 +20,11 @@ import com.github.damontecres.wholphin.ui.main.settings.favoriteOptions
|
||||||
import com.github.damontecres.wholphin.ui.playback.getTypeFor
|
import com.github.damontecres.wholphin.ui.playback.getTypeFor
|
||||||
import com.github.damontecres.wholphin.ui.toBaseItems
|
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResArgStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResProviderStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringStringProvider
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
|
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
|
@ -275,11 +281,11 @@ class HomeSettingsService
|
||||||
libraries
|
libraries
|
||||||
.mapIndexed { index, it ->
|
.mapIndexed { index, it ->
|
||||||
val parentId = it.itemId
|
val parentId = it.itemId
|
||||||
val title = getRecentlyAddedTitle(context, it)
|
val title = getRecentlyAddedTitle(it)
|
||||||
if (it.collectionType == CollectionType.LIVETV) {
|
if (it.collectionType == CollectionType.LIVETV) {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = index,
|
id = index,
|
||||||
title = context.getString(R.string.live_tv),
|
title = ResStringProvider(R.string.live_tv),
|
||||||
config = HomeRowConfig.TvPrograms(),
|
config = HomeRowConfig.TvPrograms(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -295,7 +301,7 @@ class HomeSettingsService
|
||||||
listOf(
|
listOf(
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = includedIds.size + 1,
|
id = includedIds.size + 1,
|
||||||
title = context.getString(R.string.combine_continue_next),
|
title = ResStringProvider(R.string.combine_continue_next),
|
||||||
config = HomeRowConfig.ContinueWatchingCombined(),
|
config = HomeRowConfig.ContinueWatchingCombined(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -303,12 +309,12 @@ class HomeSettingsService
|
||||||
listOf(
|
listOf(
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = includedIds.size + 1,
|
id = includedIds.size + 1,
|
||||||
title = context.getString(R.string.continue_watching),
|
title = ResStringProvider(R.string.continue_watching),
|
||||||
config = HomeRowConfig.ContinueWatching(),
|
config = HomeRowConfig.ContinueWatching(),
|
||||||
),
|
),
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = includedIds.size + 2,
|
id = includedIds.size + 2,
|
||||||
title = context.getString(R.string.next_up),
|
title = ResStringProvider(R.string.next_up),
|
||||||
config = HomeRowConfig.NextUp(),
|
config = HomeRowConfig.NextUp(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -355,7 +361,7 @@ class HomeSettingsService
|
||||||
HomeSectionType.ACTIVE_RECORDINGS -> {
|
HomeSectionType.ACTIVE_RECORDINGS -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id++,
|
id = id++,
|
||||||
title = context.getString(R.string.active_recordings),
|
title = ResStringProvider(R.string.active_recordings),
|
||||||
config = HomeRowConfig.Recordings(),
|
config = HomeRowConfig.Recordings(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -363,7 +369,7 @@ class HomeSettingsService
|
||||||
HomeSectionType.RESUME -> {
|
HomeSectionType.RESUME -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id++,
|
id = id++,
|
||||||
title = context.getString(R.string.continue_watching),
|
title = ResStringProvider(R.string.continue_watching),
|
||||||
config = HomeRowConfig.ContinueWatching(),
|
config = HomeRowConfig.ContinueWatching(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +377,7 @@ class HomeSettingsService
|
||||||
HomeSectionType.NEXT_UP -> {
|
HomeSectionType.NEXT_UP -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id++,
|
id = id++,
|
||||||
title = context.getString(R.string.next_up),
|
title = ResStringProvider(R.string.next_up),
|
||||||
config = HomeRowConfig.NextUp(),
|
config = HomeRowConfig.NextUp(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -380,7 +386,7 @@ class HomeSettingsService
|
||||||
if (userDto.tvAccess) {
|
if (userDto.tvAccess) {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id++,
|
id = id++,
|
||||||
title = context.getString(R.string.live_tv),
|
title = ResStringProvider(R.string.live_tv),
|
||||||
config = HomeRowConfig.TvPrograms(),
|
config = HomeRowConfig.TvPrograms(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -413,7 +419,7 @@ class HomeSettingsService
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id++,
|
id = id++,
|
||||||
title =
|
title =
|
||||||
context.getString(
|
ResArgStringProvider(
|
||||||
R.string.recently_added_in,
|
R.string.recently_added_in,
|
||||||
it.name ?: "",
|
it.name ?: "",
|
||||||
),
|
),
|
||||||
|
|
@ -441,7 +447,7 @@ class HomeSettingsService
|
||||||
): HomeRowConfigDisplay =
|
): HomeRowConfigDisplay =
|
||||||
when (config) {
|
when (config) {
|
||||||
is HomeRowConfig.ByParent -> {
|
is HomeRowConfig.ByParent -> {
|
||||||
val name = getItemName(config.parentId) ?: ""
|
val name = getItemName(null, config.parentId)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
|
@ -452,7 +458,7 @@ class HomeSettingsService
|
||||||
is HomeRowConfig.ContinueWatching -> {
|
is HomeRowConfig.ContinueWatching -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
context.getString(R.string.continue_watching),
|
ResStringProvider(R.string.continue_watching),
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -460,64 +466,64 @@ class HomeSettingsService
|
||||||
is HomeRowConfig.ContinueWatchingCombined -> {
|
is HomeRowConfig.ContinueWatchingCombined -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
context.getString(R.string.combine_continue_next),
|
ResStringProvider(R.string.combine_continue_next),
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.Genres -> {
|
is HomeRowConfig.Genres -> {
|
||||||
val name = getItemName(config.parentId) ?: ""
|
val title = getItemName(R.string.genres_in, config.parentId)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
context.getString(R.string.genres_in, name),
|
title,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.Studios -> {
|
is HomeRowConfig.Studios -> {
|
||||||
val name = getItemName(config.parentId) ?: ""
|
val title = getItemName(R.string.studios_in, config.parentId)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
context.getString(R.string.studios_in, name),
|
title,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.GetItems -> {
|
is HomeRowConfig.GetItems -> {
|
||||||
HomeRowConfigDisplay(id, config.name, config)
|
HomeRowConfigDisplay(id, StringStringProvider(config.name), config)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.NextUp -> {
|
is HomeRowConfig.NextUp -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
context.getString(R.string.next_up),
|
ResStringProvider(R.string.next_up),
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.RecentlyAdded -> {
|
is HomeRowConfig.RecentlyAdded -> {
|
||||||
val name = getItemName(config.parentId) ?: ""
|
val title = getItemName(R.string.recently_added_in, config.parentId)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
context.getString(R.string.recently_added_in, name),
|
title,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.RecentlyReleased -> {
|
is HomeRowConfig.RecentlyReleased -> {
|
||||||
val name = getItemName(config.parentId) ?: ""
|
val title = getItemName(R.string.recently_released_in, config.parentId)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id,
|
id,
|
||||||
context.getString(R.string.recently_released_in, name),
|
title,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.Favorite -> {
|
is HomeRowConfig.Favorite -> {
|
||||||
val name =
|
val name =
|
||||||
context.getString(
|
ResProviderStringProvider(
|
||||||
R.string.favorite_items,
|
R.string.favorite_items,
|
||||||
context.getString(favoriteOptions[config.kind]!!),
|
ResStringProvider(favoriteOptions[config.kind]!!),
|
||||||
)
|
)
|
||||||
HomeRowConfigDisplay(id, name, config)
|
HomeRowConfigDisplay(id, name, config)
|
||||||
}
|
}
|
||||||
|
|
@ -525,7 +531,7 @@ class HomeSettingsService
|
||||||
is HomeRowConfig.Recordings -> {
|
is HomeRowConfig.Recordings -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = context.getString(R.string.active_recordings),
|
title = ResStringProvider(R.string.active_recordings),
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -533,7 +539,7 @@ class HomeSettingsService
|
||||||
is HomeRowConfig.TvPrograms -> {
|
is HomeRowConfig.TvPrograms -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = context.getString(R.string.live_tv),
|
title = ResStringProvider(R.string.live_tv),
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -541,29 +547,40 @@ class HomeSettingsService
|
||||||
is HomeRowConfig.TvChannels -> {
|
is HomeRowConfig.TvChannels -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = context.getString(R.string.channels),
|
title = ResStringProvider(R.string.channels),
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.Suggestions -> {
|
is HomeRowConfig.Suggestions -> {
|
||||||
val name = getItemName(config.parentId) ?: ""
|
val title = getItemName(R.string.suggestions_for, config.parentId)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = context.getString(R.string.suggestions_for, name),
|
title = title,
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getItemName(itemId: UUID): String? =
|
private suspend fun getItemName(
|
||||||
|
@StringRes stringRes: Int?,
|
||||||
|
itemId: UUID,
|
||||||
|
default: StringProvider = StringStringProvider(""),
|
||||||
|
): StringProvider =
|
||||||
try {
|
try {
|
||||||
api.userLibraryApi
|
api.userLibraryApi
|
||||||
.getItem(itemId = itemId)
|
.getItem(itemId = itemId)
|
||||||
.content.name
|
.content.name
|
||||||
|
?.let {
|
||||||
|
if (stringRes == null) {
|
||||||
|
StringStringProvider(it)
|
||||||
|
} else {
|
||||||
|
ResArgStringProvider(stringRes, it)
|
||||||
|
}
|
||||||
|
} ?: default
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Could not get name for %s", itemId)
|
Timber.e(ex, "Could not get name for %s", itemId)
|
||||||
context.getString(R.string.unknown)
|
ResStringProvider(R.string.unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -590,7 +607,7 @@ class HomeSettingsService
|
||||||
)
|
)
|
||||||
|
|
||||||
Success(
|
Success(
|
||||||
title = context.getString(R.string.continue_watching),
|
title = ResStringProvider(R.string.continue_watching),
|
||||||
items = resume,
|
items = resume,
|
||||||
viewOptions = row.viewOptions,
|
viewOptions = row.viewOptions,
|
||||||
rowType = row,
|
rowType = row,
|
||||||
|
|
@ -609,7 +626,7 @@ class HomeSettingsService
|
||||||
)
|
)
|
||||||
|
|
||||||
Success(
|
Success(
|
||||||
title = context.getString(R.string.next_up),
|
title = ResStringProvider(R.string.next_up),
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
viewOptions = row.viewOptions,
|
viewOptions = row.viewOptions,
|
||||||
rowType = row,
|
rowType = row,
|
||||||
|
|
@ -635,7 +652,7 @@ class HomeSettingsService
|
||||||
)
|
)
|
||||||
|
|
||||||
Success(
|
Success(
|
||||||
title = context.getString(R.string.continue_watching),
|
title = ResStringProvider(R.string.continue_watching),
|
||||||
items =
|
items =
|
||||||
latestNextUpService
|
latestNextUpService
|
||||||
.buildCombined(
|
.buildCombined(
|
||||||
|
|
@ -676,8 +693,8 @@ class HomeSettingsService
|
||||||
.firstOrNull { it.itemId == row.parentId }
|
.firstOrNull { it.itemId == row.parentId }
|
||||||
|
|
||||||
val title =
|
val title =
|
||||||
library?.name?.let { context.getString(R.string.genres_in, it) }
|
library?.name?.let { ResArgStringProvider(R.string.genres_in, it) }
|
||||||
?: context.getString(R.string.genres)
|
?: ResStringProvider(R.string.genres)
|
||||||
val genres =
|
val genres =
|
||||||
items.map {
|
items.map {
|
||||||
BaseItem(
|
BaseItem(
|
||||||
|
|
@ -723,8 +740,8 @@ class HomeSettingsService
|
||||||
libraries
|
libraries
|
||||||
.firstOrNull { it.itemId == row.parentId }
|
.firstOrNull { it.itemId == row.parentId }
|
||||||
val title =
|
val title =
|
||||||
library?.name?.let { context.getString(R.string.studios_in, it) }
|
library?.name?.let { ResArgStringProvider(R.string.studios_in, it) }
|
||||||
?: context.getString(R.string.studios)
|
?: ResStringProvider(R.string.studios)
|
||||||
val studios =
|
val studios =
|
||||||
items.map {
|
items.map {
|
||||||
val imageUrl =
|
val imageUrl =
|
||||||
|
|
@ -762,7 +779,7 @@ class HomeSettingsService
|
||||||
val library =
|
val library =
|
||||||
libraries
|
libraries
|
||||||
.firstOrNull { it.itemId == row.parentId }
|
.firstOrNull { it.itemId == row.parentId }
|
||||||
val title = getRecentlyAddedTitle(context, library)
|
val title = getRecentlyAddedTitle(library)
|
||||||
val request =
|
val request =
|
||||||
GetLatestMediaRequest(
|
GetLatestMediaRequest(
|
||||||
fields = SlimItemFields,
|
fields = SlimItemFields,
|
||||||
|
|
@ -795,8 +812,8 @@ class HomeSettingsService
|
||||||
?.name
|
?.name
|
||||||
val title =
|
val title =
|
||||||
name?.let {
|
name?.let {
|
||||||
context.getString(R.string.recently_released_in, it)
|
ResArgStringProvider(R.string.recently_released_in, it)
|
||||||
} ?: context.getString(R.string.recently_released)
|
} ?: ResStringProvider(R.string.recently_released)
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
parentId = row.parentId,
|
parentId = row.parentId,
|
||||||
|
|
@ -853,7 +870,8 @@ class HomeSettingsService
|
||||||
fields = DefaultItemFields,
|
fields = DefaultItemFields,
|
||||||
)
|
)
|
||||||
|
|
||||||
val name = getItemName(row.parentId)
|
val title =
|
||||||
|
getItemName(null, row.parentId, ResStringProvider(R.string.collection))
|
||||||
if (usePaging) {
|
if (usePaging) {
|
||||||
ApiRequestPager(
|
ApiRequestPager(
|
||||||
api,
|
api,
|
||||||
|
|
@ -869,7 +887,7 @@ class HomeSettingsService
|
||||||
.map { BaseItem(it, row.viewOptions.useSeries) }
|
.map { BaseItem(it, row.viewOptions.useSeries) }
|
||||||
}.let {
|
}.let {
|
||||||
Success(
|
Success(
|
||||||
name ?: context.getString(R.string.collection),
|
title,
|
||||||
it,
|
it,
|
||||||
row.viewOptions,
|
row.viewOptions,
|
||||||
rowType = row,
|
rowType = row,
|
||||||
|
|
@ -906,7 +924,7 @@ class HomeSettingsService
|
||||||
.map { BaseItem(it, row.viewOptions.useSeries) }
|
.map { BaseItem(it, row.viewOptions.useSeries) }
|
||||||
}.let {
|
}.let {
|
||||||
Success(
|
Success(
|
||||||
row.name,
|
StringStringProvider(row.name),
|
||||||
it,
|
it,
|
||||||
row.viewOptions,
|
row.viewOptions,
|
||||||
rowType = row,
|
rowType = row,
|
||||||
|
|
@ -916,9 +934,9 @@ class HomeSettingsService
|
||||||
|
|
||||||
is HomeRowConfig.Favorite -> {
|
is HomeRowConfig.Favorite -> {
|
||||||
val title =
|
val title =
|
||||||
context.getString(
|
ResProviderStringProvider(
|
||||||
R.string.favorite_items,
|
R.string.favorite_items,
|
||||||
context.getString(favoriteOptions[row.kind]!!),
|
ResStringProvider(favoriteOptions[row.kind]!!),
|
||||||
)
|
)
|
||||||
if (row.kind == BaseItemKind.PERSON) {
|
if (row.kind == BaseItemKind.PERSON) {
|
||||||
val request =
|
val request =
|
||||||
|
|
@ -1001,7 +1019,7 @@ class HomeSettingsService
|
||||||
.map { BaseItem(it, row.viewOptions.useSeries) }
|
.map { BaseItem(it, row.viewOptions.useSeries) }
|
||||||
}.let {
|
}.let {
|
||||||
Success(
|
Success(
|
||||||
context.getString(R.string.active_recordings),
|
ResStringProvider(R.string.active_recordings),
|
||||||
it,
|
it,
|
||||||
row.viewOptions,
|
row.viewOptions,
|
||||||
rowType = row,
|
rowType = row,
|
||||||
|
|
@ -1027,7 +1045,7 @@ class HomeSettingsService
|
||||||
.map { BaseItem(it, row.viewOptions.useSeries) }
|
.map { BaseItem(it, row.viewOptions.useSeries) }
|
||||||
.let {
|
.let {
|
||||||
Success(
|
Success(
|
||||||
context.getString(R.string.live_tv),
|
ResStringProvider(R.string.live_tv),
|
||||||
it,
|
it,
|
||||||
row.viewOptions,
|
row.viewOptions,
|
||||||
rowType = row,
|
rowType = row,
|
||||||
|
|
@ -1057,7 +1075,7 @@ class HomeSettingsService
|
||||||
.toBaseItems(api, row.viewOptions.useSeries)
|
.toBaseItems(api, row.viewOptions.useSeries)
|
||||||
}.let {
|
}.let {
|
||||||
Success(
|
Success(
|
||||||
context.getString(R.string.channels),
|
ResStringProvider(R.string.channels),
|
||||||
it,
|
it,
|
||||||
row.viewOptions,
|
row.viewOptions,
|
||||||
rowType = row,
|
rowType = row,
|
||||||
|
|
@ -1070,7 +1088,7 @@ class HomeSettingsService
|
||||||
api.userLibraryApi
|
api.userLibraryApi
|
||||||
.getItem(itemId = row.parentId)
|
.getItem(itemId = row.parentId)
|
||||||
.content
|
.content
|
||||||
val title = context.getString(R.string.suggestions_for, library.name ?: "")
|
val title = ResArgStringProvider(R.string.suggestions_for, library.name ?: "")
|
||||||
val itemKind = SuggestionsWorker.getTypeForCollection(library.collectionType)
|
val itemKind = SuggestionsWorker.getTypeForCollection(library.collectionType)
|
||||||
val suggestions =
|
val suggestions =
|
||||||
itemKind?.let {
|
itemKind?.let {
|
||||||
|
|
@ -1111,7 +1129,7 @@ class HomeSettingsService
|
||||||
*/
|
*/
|
||||||
data class HomeRowConfigDisplay(
|
data class HomeRowConfigDisplay(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val title: String,
|
val title: StringProvider,
|
||||||
val config: HomeRowConfig,
|
val config: HomeRowConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1154,13 +1172,10 @@ class UnsupportedHomeSettingsVersionException(
|
||||||
val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION,
|
val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION,
|
||||||
) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion")
|
) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion")
|
||||||
|
|
||||||
fun getRecentlyAddedTitle(
|
fun getRecentlyAddedTitle(library: Library?): StringProvider =
|
||||||
context: Context,
|
|
||||||
library: Library?,
|
|
||||||
): String =
|
|
||||||
if (library?.isRecordingFolder == true) {
|
if (library?.isRecordingFolder == true) {
|
||||||
context.getString(R.string.recently_recorded)
|
ResStringProvider(R.string.recently_recorded)
|
||||||
} else {
|
} else {
|
||||||
library?.name?.let { context.getString(R.string.recently_added_in, it) }
|
library?.name?.let { ResArgStringProvider(R.string.recently_added_in, it) }
|
||||||
?: context.getString(R.string.recently_added)
|
?: ResStringProvider(R.string.recently_added)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.github.damontecres.wholphin.BuildConfig
|
|
||||||
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.AuthJellyfinPostRequest
|
import com.github.damontecres.wholphin.api.seerr.model.AuthJellyfinPostRequest
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.AuthLocalPostRequest
|
import com.github.damontecres.wholphin.api.seerr.model.AuthLocalPostRequest
|
||||||
|
|
@ -12,28 +7,22 @@ import com.github.damontecres.wholphin.api.seerr.model.PublicSettings
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.User
|
import com.github.damontecres.wholphin.api.seerr.model.User
|
||||||
import com.github.damontecres.wholphin.data.SeerrServerDao
|
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrServer
|
import com.github.damontecres.wholphin.data.model.SeerrServer
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrUser
|
import com.github.damontecres.wholphin.data.model.SeerrUser
|
||||||
import com.github.damontecres.wholphin.data.model.hasPermission
|
import com.github.damontecres.wholphin.data.model.hasPermission
|
||||||
import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient
|
import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
|
||||||
import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl
|
import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.qualifiers.ActivityContext
|
|
||||||
import dagger.hilt.android.scopes.ActivityScoped
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
@ -140,7 +129,7 @@ class SeerrServerRepository
|
||||||
serverRepository.currentUser.value?.let { jellyfinUser ->
|
serverRepository.currentUser.value?.let { jellyfinUser ->
|
||||||
// TODO Need to update server early so that cookies are saved
|
// TODO Need to update server early so that cookies are saved
|
||||||
seerrApi.update(server.url, null)
|
seerrApi.update(server.url, null)
|
||||||
val userConfig = login(seerrApi.api, authMethod, username, password)
|
val userConfig = seerrLogin(seerrApi.api, authMethod, username, password)
|
||||||
|
|
||||||
val user =
|
val user =
|
||||||
SeerrUser(
|
SeerrUser(
|
||||||
|
|
@ -174,7 +163,7 @@ class SeerrServerRepository
|
||||||
.readTimeout(6.seconds)
|
.readTimeout(6.seconds)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
login(api, authMethod, username, passwordOrApiKey)
|
seerrLogin(api, authMethod, username, passwordOrApiKey)
|
||||||
return LoadingState.Success
|
return LoadingState.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,7 +216,7 @@ data class CurrentSeerr(
|
||||||
config.hasPermission(SeerrPermission.REQUEST_4K_TV)
|
config.hasPermission(SeerrPermission.REQUEST_4K_TV)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun login(
|
suspend fun seerrLogin(
|
||||||
client: SeerrApiClient,
|
client: SeerrApiClient,
|
||||||
authMethod: SeerrAuthMethod,
|
authMethod: SeerrAuthMethod,
|
||||||
username: String?,
|
username: String?,
|
||||||
|
|
@ -259,84 +248,6 @@ private suspend fun login(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens for JF user switching in the app to also switch the Seerr user/server
|
|
||||||
*/
|
|
||||||
@ActivityScoped
|
|
||||||
class UserSwitchListener
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@param:ActivityContext private val context: Context,
|
|
||||||
private val serverRepository: ServerRepository,
|
|
||||||
private val seerrServerRepository: SeerrServerRepository,
|
|
||||||
private val seerrServerDao: SeerrServerDao,
|
|
||||||
private val seerrApi: SeerrApi,
|
|
||||||
private val homeSettingsService: HomeSettingsService,
|
|
||||||
) {
|
|
||||||
init {
|
|
||||||
context as AppCompatActivity
|
|
||||||
context.lifecycleScope.launchIO {
|
|
||||||
serverRepository.currentUser.asFlow().collect { user ->
|
|
||||||
Timber.d("New user")
|
|
||||||
seerrServerRepository.clear()
|
|
||||||
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }
|
|
||||||
if (user != null) {
|
|
||||||
switchUser(user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun switchUser(user: JellyfinUser) =
|
|
||||||
supervisorScope {
|
|
||||||
// Check for home settings
|
|
||||||
launchIO {
|
|
||||||
homeSettingsService.loadCurrentSettings(user.id)
|
|
||||||
}
|
|
||||||
if (BuildConfig.DISCOVER_ENABLED) {
|
|
||||||
// Check for seerr server
|
|
||||||
launchIO {
|
|
||||||
seerrServerDao
|
|
||||||
.getUsersByJellyfinUser(user.rowId)
|
|
||||||
.lastOrNull()
|
|
||||||
?.let { seerrUser ->
|
|
||||||
val server =
|
|
||||||
seerrServerDao.getServer(seerrUser.serverId)?.server
|
|
||||||
if (server != null) {
|
|
||||||
Timber.i("Found a seerr user & server")
|
|
||||||
try {
|
|
||||||
seerrApi.update(server.url, seerrUser.credential)
|
|
||||||
val userConfig =
|
|
||||||
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
|
||||||
login(
|
|
||||||
seerrApi.api,
|
|
||||||
seerrUser.authMethod,
|
|
||||||
seerrUser.username,
|
|
||||||
seerrUser.password,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
seerrApi.api.usersApi.authMeGet()
|
|
||||||
}
|
|
||||||
seerrServerRepository.set(
|
|
||||||
server,
|
|
||||||
seerrUser,
|
|
||||||
userConfig,
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Timber.w(
|
|
||||||
ex,
|
|
||||||
"Error logging into %s",
|
|
||||||
server.url,
|
|
||||||
)
|
|
||||||
seerrServerRepository.error(server, seerrUser, ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CurrentSeerr?.imageUrlBuilder(
|
fun CurrentSeerr?.imageUrlBuilder(
|
||||||
imageType: ImageType,
|
imageType: ImageType,
|
||||||
path: String?,
|
path: String?,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.github.damontecres.wholphin.BuildConfig
|
||||||
|
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||||
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import dagger.hilt.android.qualifiers.ActivityContext
|
||||||
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for JF user switching in the app to also switch other settings like Seerr user/server
|
||||||
|
*/
|
||||||
|
@ActivityScoped
|
||||||
|
class UserSwitchListener
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@param:ActivityContext private val context: Context,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
|
private val seerrServerDao: SeerrServerDao,
|
||||||
|
private val seerrApi: SeerrApi,
|
||||||
|
private val homeSettingsService: HomeSettingsService,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
context as AppCompatActivity
|
||||||
|
context.lifecycleScope.launchDefault {
|
||||||
|
serverRepository.currentUser.asFlow().collect { user ->
|
||||||
|
Timber.d("New user")
|
||||||
|
seerrServerRepository.clear()
|
||||||
|
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }
|
||||||
|
if (user != null) {
|
||||||
|
switchUser(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun switchUser(user: JellyfinUser) =
|
||||||
|
supervisorScope {
|
||||||
|
// Switch the locale to either the user's choice or the system default (empty)
|
||||||
|
val localeList =
|
||||||
|
user.uiLanguage?.let { LocaleListCompat.forLanguageTags(it) }
|
||||||
|
?: LocaleListCompat.getEmptyLocaleList()
|
||||||
|
Timber.i("Switching locale to %s", localeList)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
AppCompatDelegate.setApplicationLocales(localeList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for home settings
|
||||||
|
launchIO {
|
||||||
|
homeSettingsService.loadCurrentSettings(user.id)
|
||||||
|
}
|
||||||
|
if (BuildConfig.DISCOVER_ENABLED) {
|
||||||
|
// Check for seerr server
|
||||||
|
launchIO {
|
||||||
|
seerrServerDao
|
||||||
|
.getUsersByJellyfinUser(user.rowId)
|
||||||
|
.lastOrNull()
|
||||||
|
?.let { seerrUser ->
|
||||||
|
val server =
|
||||||
|
seerrServerDao.getServer(seerrUser.serverId)?.server
|
||||||
|
if (server != null) {
|
||||||
|
Timber.i("Found a seerr user & server")
|
||||||
|
try {
|
||||||
|
seerrApi.update(server.url, seerrUser.credential)
|
||||||
|
val userConfig =
|
||||||
|
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
||||||
|
seerrLogin(
|
||||||
|
seerrApi.api,
|
||||||
|
seerrUser.authMethod,
|
||||||
|
seerrUser.username,
|
||||||
|
seerrUser.password,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
seerrApi.api.usersApi.authMeGet()
|
||||||
|
}
|
||||||
|
seerrServerRepository.set(
|
||||||
|
server,
|
||||||
|
seerrUser,
|
||||||
|
userConfig,
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(
|
||||||
|
ex,
|
||||||
|
"Error logging into %s",
|
||||||
|
server.url,
|
||||||
|
)
|
||||||
|
seerrServerRepository.error(server, seerrUser, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
|
@ -15,7 +15,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
|
@ -184,15 +184,15 @@ fun ContextMenu(
|
||||||
) {
|
) {
|
||||||
val item = contextMenu.item
|
val item = contextMenu.item
|
||||||
val chosenStreams = contextMenu.chosenStreams
|
val chosenStreams = contextMenu.chosenStreams
|
||||||
val context = LocalContext.current
|
val resources = LocalResources.current
|
||||||
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
var showPlayWithDialog by remember { mutableStateOf(false) }
|
var showPlayWithDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val dialogItems =
|
val dialogItems =
|
||||||
remember(context, item, chosenStreams) {
|
remember(resources, item, chosenStreams) {
|
||||||
buildContextMenuItems(
|
buildContextMenuItems(
|
||||||
context = context,
|
resources = resources,
|
||||||
item = item,
|
item = item,
|
||||||
watched = item.played,
|
watched = item.played,
|
||||||
favorite = item.favorite,
|
favorite = item.favorite,
|
||||||
|
|
@ -208,7 +208,7 @@ fun ContextMenu(
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseVersionParams(
|
chooseVersionParams(
|
||||||
context,
|
resources,
|
||||||
item.data.mediaSources.orEmpty(),
|
item.data.mediaSources.orEmpty(),
|
||||||
chosenStreams?.source?.id?.toUUIDOrNull(),
|
chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
) { idx ->
|
) { idx ->
|
||||||
|
|
@ -225,7 +225,7 @@ fun ContextMenu(
|
||||||
)?.let { source ->
|
)?.let { source ->
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
chooseStream(
|
chooseStream(
|
||||||
context = context,
|
resources = resources,
|
||||||
streams = source.mediaStreams.orEmpty(),
|
streams = source.mediaStreams.orEmpty(),
|
||||||
type = type,
|
type = type,
|
||||||
currentIndex =
|
currentIndex =
|
||||||
|
|
@ -290,7 +290,7 @@ fun ContextMenu(
|
||||||
if (showPlayWithDialog) {
|
if (showPlayWithDialog) {
|
||||||
val dialogItems =
|
val dialogItems =
|
||||||
remember {
|
remember {
|
||||||
buildPlayWith(context) { transcode, backend ->
|
buildPlayWith(resources) { transcode, backend ->
|
||||||
onDismissRequest.invoke()
|
onDismissRequest.invoke()
|
||||||
actions.navigateTo(
|
actions.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
|
|
@ -315,7 +315,7 @@ fun ContextMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildContextMenuItems(
|
private fun buildContextMenuItems(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
seriesId: UUID?,
|
seriesId: UUID?,
|
||||||
sourceId: UUID?,
|
sourceId: UUID?,
|
||||||
|
|
@ -339,7 +339,7 @@ private fun buildContextMenuItems(
|
||||||
if (showGoTo) {
|
if (showGoTo) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to),
|
resources.getString(R.string.go_to),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -351,7 +351,7 @@ private fun buildContextMenuItems(
|
||||||
if (item.playbackPosition > Duration.ZERO) {
|
if (item.playbackPosition > Duration.ZERO) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.resume),
|
resources.getString(R.string.resume),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
|
|
@ -366,7 +366,7 @@ private fun buildContextMenuItems(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.restart),
|
resources.getString(R.string.restart),
|
||||||
Icons.Default.Refresh,
|
Icons.Default.Refresh,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -381,7 +381,7 @@ private fun buildContextMenuItems(
|
||||||
} else {
|
} else {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play),
|
resources.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
|
|
@ -407,9 +407,9 @@ private fun buildContextMenuItems(
|
||||||
if (audioCount > 1) {
|
if (audioCount > 1) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(
|
resources.getString(
|
||||||
R.string.choose_stream,
|
R.string.choose_stream,
|
||||||
context.getString(R.string.audio),
|
resources.getString(R.string.audio),
|
||||||
),
|
),
|
||||||
R.string.fa_volume_low,
|
R.string.fa_volume_low,
|
||||||
dismissOnClick = false,
|
dismissOnClick = false,
|
||||||
|
|
@ -421,9 +421,9 @@ private fun buildContextMenuItems(
|
||||||
if (subtitleCount > 0) {
|
if (subtitleCount > 0) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(
|
resources.getString(
|
||||||
R.string.choose_stream,
|
R.string.choose_stream,
|
||||||
context.getString(R.string.subtitles),
|
resources.getString(R.string.subtitles),
|
||||||
),
|
),
|
||||||
R.string.fa_closed_captioning,
|
R.string.fa_closed_captioning,
|
||||||
dismissOnClick = false,
|
dismissOnClick = false,
|
||||||
|
|
@ -436,9 +436,9 @@ private fun buildContextMenuItems(
|
||||||
if (sources.size > 1) {
|
if (sources.size > 1) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(
|
resources.getString(
|
||||||
R.string.choose_stream,
|
R.string.choose_stream,
|
||||||
context.getString(R.string.version),
|
resources.getString(R.string.version),
|
||||||
),
|
),
|
||||||
R.string.fa_file_video,
|
R.string.fa_file_video,
|
||||||
dismissOnClick = false,
|
dismissOnClick = false,
|
||||||
|
|
@ -452,7 +452,7 @@ private fun buildContextMenuItems(
|
||||||
if (item.type == BaseItemKind.MUSIC_ALBUM) {
|
if (item.type == BaseItemKind.MUSIC_ALBUM) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.add_to_queue),
|
resources.getString(R.string.add_to_queue),
|
||||||
Icons.Default.Add,
|
Icons.Default.Add,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -472,7 +472,7 @@ private fun buildContextMenuItems(
|
||||||
if (canDelete) {
|
if (canDelete) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.delete),
|
resources.getString(R.string.delete),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
iconColor = Color.Red.copy(alpha = .8f),
|
iconColor = Color.Red.copy(alpha = .8f),
|
||||||
dismissOnClick = false,
|
dismissOnClick = false,
|
||||||
|
|
@ -525,7 +525,7 @@ private fun buildContextMenuItems(
|
||||||
item.data.albumId?.let { albumId ->
|
item.data.albumId?.let { albumId ->
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_album),
|
resources.getString(R.string.go_to_album),
|
||||||
R.string.fa_compact_disc,
|
R.string.fa_compact_disc,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -542,7 +542,7 @@ private fun buildContextMenuItems(
|
||||||
item.data.artistItems?.firstOrNull()?.id?.let { artistId ->
|
item.data.artistItems?.firstOrNull()?.id?.let { artistId ->
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_artist),
|
resources.getString(R.string.go_to_artist),
|
||||||
R.string.fa_user,
|
R.string.fa_user,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -559,7 +559,7 @@ private fun buildContextMenuItems(
|
||||||
seriesId?.let {
|
seriesId?.let {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_series),
|
resources.getString(R.string.go_to_series),
|
||||||
Icons.AutoMirrored.Filled.ArrowForward,
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -576,7 +576,7 @@ private fun buildContextMenuItems(
|
||||||
if (item.data.mediaSources?.isNotEmpty() == true) {
|
if (item.data.mediaSources?.isNotEmpty() == true) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.media_information),
|
resources.getString(R.string.media_information),
|
||||||
Icons.Default.Info,
|
Icons.Default.Info,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -587,7 +587,7 @@ private fun buildContextMenuItems(
|
||||||
if (showStreamChoices && canClearChosenStreams) {
|
if (showStreamChoices && canClearChosenStreams) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.clear_track_choices),
|
resources.getString(R.string.clear_track_choices),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -598,7 +598,7 @@ private fun buildContextMenuItems(
|
||||||
if (item.type in supportedPlayableTypes) {
|
if (item.type in supportedPlayableTypes) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play_with),
|
resources.getString(R.string.play_with),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
dismissOnClick = false,
|
dismissOnClick = false,
|
||||||
onClick = onClickPlayWith,
|
onClick = onClickPlayWith,
|
||||||
|
|
@ -619,13 +619,13 @@ private fun buildContextMenuItems(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPlayWith(
|
private fun buildPlayWith(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
onClick: (Boolean, PlayerBackend?) -> Unit,
|
onClick: (Boolean, PlayerBackend?) -> Unit,
|
||||||
) = buildList {
|
) = buildList {
|
||||||
val entries =
|
val entries =
|
||||||
PlayerBackend.entries
|
PlayerBackend.entries
|
||||||
.filterNot { it == PlayerBackend.UNRECOGNIZED }
|
.filterNot { it == PlayerBackend.UNRECOGNIZED }
|
||||||
.zip(context.resources.getStringArray(R.array.player_backend_options))
|
.zip(resources.getStringArray(R.array.player_backend_options))
|
||||||
.filterNot { it.first == PlayerBackend.PREFER_MPV }
|
.filterNot { it.first == PlayerBackend.PREFER_MPV }
|
||||||
entries.forEach { (backend, title) ->
|
entries.forEach { (backend, title) ->
|
||||||
add(
|
add(
|
||||||
|
|
@ -639,7 +639,7 @@ private fun buildPlayWith(
|
||||||
}
|
}
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.transcoding),
|
resources.getString(R.string.transcoding),
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
onClick.invoke(true, null)
|
onClick.invoke(true, null)
|
||||||
|
|
@ -691,11 +691,11 @@ fun ContextMenu(
|
||||||
item: ContextMenu.ForMusic,
|
item: ContextMenu.ForMusic,
|
||||||
actions: MusicContextActions,
|
actions: MusicContextActions,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val resources = LocalResources.current
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
val dialogItems =
|
val dialogItems =
|
||||||
remember(context, item, actions) {
|
remember(resources, item, actions) {
|
||||||
buildContextForMusic(context, item, actions, onClickDelete = {
|
buildContextForMusic(resources, item, actions, onClickDelete = {
|
||||||
showDeleteDialog = true
|
showDeleteDialog = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -720,7 +720,7 @@ fun ContextMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildContextForMusic(
|
fun buildContextForMusic(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
music: ContextMenu.ForMusic,
|
music: ContextMenu.ForMusic,
|
||||||
actions: MusicContextActions,
|
actions: MusicContextActions,
|
||||||
onClickDelete: () -> Unit,
|
onClickDelete: () -> Unit,
|
||||||
|
|
@ -730,7 +730,7 @@ fun buildContextForMusic(
|
||||||
val index = music.index
|
val index = music.index
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play),
|
resources.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
|
|
@ -740,7 +740,7 @@ fun buildContextForMusic(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play_next),
|
resources.getString(R.string.play_next),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
|
|
@ -751,7 +751,7 @@ fun buildContextForMusic(
|
||||||
if (music.canRemoveFromQueue) {
|
if (music.canRemoveFromQueue) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.remove_from_queue),
|
resources.getString(R.string.remove_from_queue),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -761,7 +761,7 @@ fun buildContextForMusic(
|
||||||
} else {
|
} else {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.add_to_queue),
|
resources.getString(R.string.add_to_queue),
|
||||||
Icons.Default.Add,
|
Icons.Default.Add,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -781,7 +781,7 @@ fun buildContextForMusic(
|
||||||
if (music.canDelete) {
|
if (music.canDelete) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.delete),
|
resources.getString(R.string.delete),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
iconColor = Color.Red.copy(alpha = .8f),
|
iconColor = Color.Red.copy(alpha = .8f),
|
||||||
dismissOnClick = false,
|
dismissOnClick = false,
|
||||||
|
|
@ -803,7 +803,7 @@ fun buildContextForMusic(
|
||||||
if (item.type == BaseItemKind.AUDIO && item.data.albumId != null) {
|
if (item.type == BaseItemKind.AUDIO && item.data.albumId != null) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_album),
|
resources.getString(R.string.go_to_album),
|
||||||
R.string.fa_compact_disc,
|
R.string.fa_compact_disc,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -814,7 +814,7 @@ fun buildContextForMusic(
|
||||||
if ((item.type == BaseItemKind.AUDIO || item.type == BaseItemKind.MUSIC_ALBUM) && item.data.artistItems?.isNotEmpty() == true) {
|
if ((item.type == BaseItemKind.AUDIO || item.type == BaseItemKind.MUSIC_ALBUM) && item.data.artistItems?.isNotEmpty() == true) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_artist),
|
resources.getString(R.string.go_to_artist),
|
||||||
R.string.fa_user,
|
R.string.fa_user,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -834,11 +834,11 @@ fun ContextMenu(
|
||||||
item: ContextMenu.ForQueue,
|
item: ContextMenu.ForQueue,
|
||||||
actions: QueueContextActions,
|
actions: QueueContextActions,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val resources = LocalResources.current
|
||||||
val dialogItems =
|
val dialogItems =
|
||||||
remember(context, item, actions) {
|
remember(resources, item, actions) {
|
||||||
buildContextForMusicQueue(
|
buildContextForMusicQueue(
|
||||||
context,
|
resources,
|
||||||
item.item,
|
item.item,
|
||||||
item.index,
|
item.index,
|
||||||
actions,
|
actions,
|
||||||
|
|
@ -855,7 +855,7 @@ fun ContextMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildContextForMusicQueue(
|
fun buildContextForMusicQueue(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
item: AudioItem,
|
item: AudioItem,
|
||||||
index: Int,
|
index: Int,
|
||||||
actions: QueueContextActions,
|
actions: QueueContextActions,
|
||||||
|
|
@ -863,7 +863,7 @@ fun buildContextForMusicQueue(
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play),
|
resources.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
|
|
@ -873,7 +873,7 @@ fun buildContextForMusicQueue(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play_next),
|
resources.getString(R.string.play_next),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
|
|
@ -883,7 +883,7 @@ fun buildContextForMusicQueue(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.remove_from_queue),
|
resources.getString(R.string.remove_from_queue),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -893,7 +893,7 @@ fun buildContextForMusicQueue(
|
||||||
if (item.albumId != null) {
|
if (item.albumId != null) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_album),
|
resources.getString(R.string.go_to_album),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -904,7 +904,7 @@ fun buildContextForMusicQueue(
|
||||||
if (item.artistId != null) {
|
if (item.artistId != null) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_artist),
|
resources.getString(R.string.go_to_artist),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
dismissOnClick = true,
|
dismissOnClick = true,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -608,14 +608,14 @@ fun ConfirmDeleteDialog(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chooseVersionParams(
|
fun chooseVersionParams(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
sources: List<MediaSourceInfo>,
|
sources: List<MediaSourceInfo>,
|
||||||
chosenSourceId: UUID?,
|
chosenSourceId: UUID?,
|
||||||
onClick: (Int) -> Unit,
|
onClick: (Int) -> Unit,
|
||||||
): DialogParams =
|
): DialogParams =
|
||||||
DialogParams(
|
DialogParams(
|
||||||
fromLongClick = false,
|
fromLongClick = false,
|
||||||
title = context.getString(R.string.choose_stream, context.getString(R.string.version)),
|
title = resources.getString(R.string.choose_stream, resources.getString(R.string.version)),
|
||||||
items =
|
items =
|
||||||
sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source ->
|
sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source ->
|
||||||
val uuid = source.id?.toUUIDOrNull()
|
val uuid = source.id?.toUUIDOrNull()
|
||||||
|
|
@ -649,7 +649,7 @@ fun resourceFor(type: MediaStreamType): Int =
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chooseStream(
|
fun chooseStream(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
streams: List<MediaStream>,
|
streams: List<MediaStream>,
|
||||||
currentIndex: Int?,
|
currentIndex: Int?,
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
|
|
@ -658,7 +658,7 @@ fun chooseStream(
|
||||||
): DialogParams =
|
): DialogParams =
|
||||||
DialogParams(
|
DialogParams(
|
||||||
fromLongClick = false,
|
fromLongClick = false,
|
||||||
title = context.getString(R.string.choose_stream, context.getString(resourceFor(type))),
|
title = resources.getString(R.string.choose_stream, resources.getString(resourceFor(type))),
|
||||||
items =
|
items =
|
||||||
buildList {
|
buildList {
|
||||||
if (type == MediaStreamType.SUBTITLE) {
|
if (type == MediaStreamType.SUBTITLE) {
|
||||||
|
|
@ -700,7 +700,7 @@ fun chooseStream(
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
}.mapIndexed { index, stream ->
|
}.mapIndexed { index, stream ->
|
||||||
val simpleStream = SimpleMediaStream.from(context, stream, true)
|
val simpleStream = SimpleMediaStream.from(resources, stream, true)
|
||||||
DialogItem(
|
DialogItem(
|
||||||
selected = currentIndex == stream.index,
|
selected = currentIndex == stream.index,
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -112,7 +111,7 @@ fun ItemGrid(
|
||||||
}
|
}
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = destination.title ?: destination.titleRes?.let { stringResource(it) } ?: "",
|
text = destination.title.getString(),
|
||||||
style = MaterialTheme.typography.displayMedium,
|
style = MaterialTheme.typography.displayMedium,
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
|
@ -132,14 +133,14 @@ fun TimeRemaining(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val resources = LocalResources.current
|
||||||
val now by LocalClock.current.now
|
val now by LocalClock.current.now
|
||||||
val remainingStr =
|
val remainingStr =
|
||||||
remember(remaining, now) {
|
remember(remaining, now, resources) {
|
||||||
val endTimeStr = getTimeFormatter().format(now.plusSeconds(remaining.inWholeSeconds))
|
val endTimeStr = getTimeFormatter().format(now.plusSeconds(remaining.inWholeSeconds))
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
dot()
|
dot()
|
||||||
append(context.getString(R.string.ends_at, endTimeStr))
|
append(resources.getString(R.string.ends_at, endTimeStr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.toBaseItems
|
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
|
|
@ -111,13 +112,13 @@ class RecommendedViewModel
|
||||||
it.copy(
|
it.copy(
|
||||||
loading = LoadingState.Loading,
|
loading = LoadingState.Loading,
|
||||||
rows =
|
rows =
|
||||||
recommendedRows.map { HomeRowLoadingState.Loading(context.getString(it.title)) } +
|
recommendedRows.map { HomeRowLoadingState.Loading(ResStringProvider(it.title)) } +
|
||||||
listOf(HomeRowLoadingState.Loading(context.getString(R.string.suggestions))),
|
listOf(HomeRowLoadingState.Loading(ResStringProvider(R.string.suggestions))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val jobs =
|
val jobs =
|
||||||
recommendedRows.mapIndexed { index, row ->
|
recommendedRows.mapIndexed { index, row ->
|
||||||
val title = context.getString(row.title)
|
val title = ResStringProvider(row.title)
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result =
|
val result =
|
||||||
try {
|
try {
|
||||||
|
|
@ -173,7 +174,7 @@ class RecommendedViewModel
|
||||||
|
|
||||||
private fun fetchSuggestions() {
|
private fun fetchSuggestions() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val title = context.getString(R.string.suggestions)
|
val title = ResStringProvider(R.string.suggestions)
|
||||||
try {
|
try {
|
||||||
suggestionService
|
suggestionService
|
||||||
.getSuggestionsFlow(parentId, suggestionsType)
|
.getSuggestionsFlow(parentId, suggestionsType)
|
||||||
|
|
@ -298,7 +299,6 @@ class RecommendedViewModel
|
||||||
navigationManager.navigateTo(
|
navigationManager.navigateTo(
|
||||||
Destination.ItemGrid(
|
Destination.ItemGrid(
|
||||||
title = row.title,
|
title = row.title,
|
||||||
titleRes = recommendedRow.title,
|
|
||||||
request = recommendedRow.request,
|
request = recommendedRow.request,
|
||||||
requestHandler = recommendedRow.handler,
|
requestHandler = recommendedRow.handler,
|
||||||
initialPosition = row.items.size,
|
initialPosition = row.items.size,
|
||||||
|
|
@ -313,7 +313,6 @@ class RecommendedViewModel
|
||||||
navigationManager.navigateTo(
|
navigationManager.navigateTo(
|
||||||
Destination.ItemGrid(
|
Destination.ItemGrid(
|
||||||
title = row.title,
|
title = row.title,
|
||||||
titleRes = R.string.suggestions,
|
|
||||||
request =
|
request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
ids = row.items.mapNotNull { it?.id },
|
ids = row.items.mapNotNull { it?.id },
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -69,7 +69,7 @@ fun VideoStreamDetails(
|
||||||
numberOfVersions: Int = 0,
|
numberOfVersions: Int = 0,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val resources = LocalResources.current
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -90,7 +90,13 @@ fun VideoStreamDetails(
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val range = formatVideoRange(context, it.videoRange, it.videoRangeType, it.videoDoViTitle)
|
val range =
|
||||||
|
formatVideoRange(
|
||||||
|
resources,
|
||||||
|
it.videoRange,
|
||||||
|
it.videoRangeType,
|
||||||
|
it.videoDoViTitle,
|
||||||
|
)
|
||||||
resName.concatWithSpace(range)
|
resName.concatWithSpace(range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,11 +114,11 @@ fun VideoStreamDetails(
|
||||||
val audio =
|
val audio =
|
||||||
remember(audioCount, audioStream) {
|
remember(audioCount, audioStream) {
|
||||||
if (audioCount == 0 || audioStream == null) {
|
if (audioCount == 0 || audioStream == null) {
|
||||||
context.getString(R.string.none)
|
resources.getString(R.string.none)
|
||||||
} else {
|
} else {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
languageName(audioStream.language),
|
languageName(audioStream.language),
|
||||||
formatAudioCodec(context, audioStream.codec, audioStream.profile),
|
formatAudioCodec(resources, audioStream.codec, audioStream.profile),
|
||||||
audioStream.channelLayout,
|
audioStream.channelLayout,
|
||||||
).joinToString(" ")
|
).joinToString(" ")
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +139,7 @@ fun VideoStreamDetails(
|
||||||
remember(subtitleCount, subtitleStream) {
|
remember(subtitleCount, subtitleStream) {
|
||||||
if (subtitleCount > 0 && subtitleStream == null) {
|
if (subtitleCount > 0 && subtitleStream == null) {
|
||||||
disabled = true
|
disabled = true
|
||||||
context.getString(R.string.none)
|
resources.getString(R.string.none)
|
||||||
} else if (subtitleCount == 0 || subtitleStream == null) {
|
} else if (subtitleCount == 0 || subtitleStream == null) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.data
|
package com.github.damontecres.wholphin.ui.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
|
@ -13,7 +13,7 @@ import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -66,7 +66,7 @@ fun ItemDetailsDialog(
|
||||||
showFilePath: Boolean,
|
showFilePath: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val resources = LocalResources.current
|
||||||
// Extract stringResource calls outside of ScrollableDialog's non-composable lambda
|
// Extract stringResource calls outside of ScrollableDialog's non-composable lambda
|
||||||
val pathLabel = stringResource(R.string.path)
|
val pathLabel = stringResource(R.string.path)
|
||||||
val fileSizeLabel = stringResource(R.string.file_size)
|
val fileSizeLabel = stringResource(R.string.file_size)
|
||||||
|
|
@ -164,8 +164,8 @@ fun ItemDetailsDialog(
|
||||||
itemsIndexed(videoStreams) { index, stream ->
|
itemsIndexed(videoStreams) { index, stream ->
|
||||||
MediaInfoSection(
|
MediaInfoSection(
|
||||||
title = titleIndex(videoLabel, index, videoStreams.size),
|
title = titleIndex(videoLabel, index, videoStreams.size),
|
||||||
items = remember { buildVideoStreamInfo(context, stream) },
|
items = remember { buildVideoStreamInfo(resources, stream) },
|
||||||
additional = remember { buildVideoStreamInfoAdditional(context, stream) },
|
additional = remember { buildVideoStreamInfoAdditional(resources, stream) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,7 +184,7 @@ fun ItemDetailsDialog(
|
||||||
groupIndex * 3 + index,
|
groupIndex * 3 + index,
|
||||||
audioStreams.size,
|
audioStreams.size,
|
||||||
),
|
),
|
||||||
items = buildAudioStreamInfo(context, stream),
|
items = buildAudioStreamInfo(resources, stream),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +210,7 @@ fun ItemDetailsDialog(
|
||||||
groupIndex * 3 + index,
|
groupIndex * 3 + index,
|
||||||
subtitleStreams.size,
|
subtitleStreams.size,
|
||||||
),
|
),
|
||||||
items = buildSubtitleStreamInfo(context, stream, showFilePath),
|
items = buildSubtitleStreamInfo(resources, stream, showFilePath),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -301,25 +301,25 @@ fun titleIndex(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildVideoStreamInfo(
|
private fun buildVideoStreamInfo(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
stream: MediaStream,
|
stream: MediaStream,
|
||||||
): List<Pair<String, String>> =
|
): List<Pair<String, String>> =
|
||||||
buildList {
|
buildList {
|
||||||
val yesStr = context.getString(R.string.yes)
|
val yesStr = resources.getString(R.string.yes)
|
||||||
val noStr = context.getString(R.string.no)
|
val noStr = resources.getString(R.string.no)
|
||||||
|
|
||||||
val titleLabel = context.getString(R.string.title)
|
val titleLabel = resources.getString(R.string.title)
|
||||||
val codecLabel = context.getString(R.string.codec)
|
val codecLabel = resources.getString(R.string.codec)
|
||||||
val resolutionLabel = context.getString(R.string.resolution)
|
val resolutionLabel = resources.getString(R.string.resolution)
|
||||||
val aspectRatioLabel = context.getString(R.string.aspect_ratio)
|
val aspectRatioLabel = resources.getString(R.string.aspect_ratio)
|
||||||
val framerateLabel = context.getString(R.string.framerate)
|
val framerateLabel = resources.getString(R.string.framerate)
|
||||||
val bitrateLabel = context.getString(R.string.bitrate)
|
val bitrateLabel = resources.getString(R.string.bitrate)
|
||||||
val profileLabel = context.getString(R.string.profile)
|
val profileLabel = resources.getString(R.string.profile)
|
||||||
val levelLabel = context.getString(R.string.level)
|
val levelLabel = resources.getString(R.string.level)
|
||||||
val interlacedLabel = context.getString(R.string.interlaced)
|
val interlacedLabel = resources.getString(R.string.interlaced)
|
||||||
val videoRangeLabel = context.getString(R.string.video_range)
|
val videoRangeLabel = resources.getString(R.string.video_range)
|
||||||
val sdrStr = context.getString(R.string.sdr)
|
val sdrStr = resources.getString(R.string.sdr)
|
||||||
val hdrStr = context.getString(R.string.hdr)
|
val hdrStr = resources.getString(R.string.hdr)
|
||||||
|
|
||||||
stream.title?.let { add(titleLabel to it) }
|
stream.title?.let { add(titleLabel to it) }
|
||||||
stream.codec?.let { add(codecLabel to it.uppercase()) }
|
stream.codec?.let { add(codecLabel to it.uppercase()) }
|
||||||
|
|
@ -350,31 +350,31 @@ private fun buildVideoStreamInfo(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildVideoStreamInfoAdditional(
|
private fun buildVideoStreamInfoAdditional(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
stream: MediaStream,
|
stream: MediaStream,
|
||||||
): List<Pair<String, String>> =
|
): List<Pair<String, String>> =
|
||||||
buildList {
|
buildList {
|
||||||
val yesStr = context.getString(R.string.yes)
|
val yesStr = resources.getString(R.string.yes)
|
||||||
val noStr = context.getString(R.string.no)
|
val noStr = resources.getString(R.string.no)
|
||||||
|
|
||||||
val avcLabel = context.getString(R.string.avc)
|
val avcLabel = resources.getString(R.string.avc)
|
||||||
val anamorphicLabel = context.getString(R.string.anamorphic)
|
val anamorphicLabel = resources.getString(R.string.anamorphic)
|
||||||
val bitDepthLabel = context.getString(R.string.bit_depth)
|
val bitDepthLabel = resources.getString(R.string.bit_depth)
|
||||||
|
|
||||||
val videoRangeTypeLabel = context.getString(R.string.video_range_type)
|
val videoRangeTypeLabel = resources.getString(R.string.video_range_type)
|
||||||
val colorSpaceLabel = context.getString(R.string.color_space)
|
val colorSpaceLabel = resources.getString(R.string.color_space)
|
||||||
val colorTransferLabel = context.getString(R.string.color_transfer)
|
val colorTransferLabel = resources.getString(R.string.color_transfer)
|
||||||
val colorPrimariesLabel = context.getString(R.string.color_primaries)
|
val colorPrimariesLabel = resources.getString(R.string.color_primaries)
|
||||||
val pixelFormatLabel = context.getString(R.string.pixel_format)
|
val pixelFormatLabel = resources.getString(R.string.pixel_format)
|
||||||
val refFramesLabel = context.getString(R.string.ref_frames)
|
val refFramesLabel = resources.getString(R.string.ref_frames)
|
||||||
val nalLabel = context.getString(R.string.nal)
|
val nalLabel = resources.getString(R.string.nal)
|
||||||
val dolbyVisionLabel = context.getString(R.string.dolby_vision)
|
val dolbyVisionLabel = resources.getString(R.string.dolby_vision)
|
||||||
|
|
||||||
val sdrStr = context.getString(R.string.sdr)
|
val sdrStr = resources.getString(R.string.sdr)
|
||||||
val hdr10Str = context.getString(R.string.hdr10)
|
val hdr10Str = resources.getString(R.string.hdr10)
|
||||||
val hdr10PlusStr = context.getString(R.string.hdr10_plus)
|
val hdr10PlusStr = resources.getString(R.string.hdr10_plus)
|
||||||
val hlgStr = context.getString(R.string.hlg)
|
val hlgStr = resources.getString(R.string.hlg)
|
||||||
val bitUnit = context.getString(R.string.bit_unit)
|
val bitUnit = resources.getString(R.string.bit_unit)
|
||||||
|
|
||||||
stream.isAvc?.let { add(avcLabel to if (it) yesStr else noStr) }
|
stream.isAvc?.let { add(avcLabel to if (it) yesStr else noStr) }
|
||||||
stream.isAnamorphic?.let { add(anamorphicLabel to if (it) yesStr else noStr) }
|
stream.isAnamorphic?.let { add(anamorphicLabel to if (it) yesStr else noStr) }
|
||||||
|
|
@ -394,7 +394,7 @@ private fun buildVideoStreamInfoAdditional(
|
||||||
VideoRangeType.DOVI_WITH_HDR10,
|
VideoRangeType.DOVI_WITH_HDR10,
|
||||||
VideoRangeType.DOVI_WITH_HLG,
|
VideoRangeType.DOVI_WITH_HLG,
|
||||||
VideoRangeType.DOVI_WITH_SDR,
|
VideoRangeType.DOVI_WITH_SDR,
|
||||||
-> context.getString(R.string.dolby_vision)
|
-> resources.getString(R.string.dolby_vision)
|
||||||
|
|
||||||
VideoRangeType.UNKNOWN -> null
|
VideoRangeType.UNKNOWN -> null
|
||||||
|
|
||||||
|
|
@ -412,27 +412,27 @@ private fun buildVideoStreamInfoAdditional(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAudioStreamInfo(
|
private fun buildAudioStreamInfo(
|
||||||
context: Context,
|
resources: android.content.res.Resources,
|
||||||
stream: MediaStream,
|
stream: MediaStream,
|
||||||
): List<Pair<String, String>> =
|
): List<Pair<String, String>> =
|
||||||
buildList {
|
buildList {
|
||||||
val titleLabel = context.getString(R.string.title)
|
val titleLabel = resources.getString(R.string.title)
|
||||||
val languageLabel = context.getString(R.string.language)
|
val languageLabel = resources.getString(R.string.language)
|
||||||
val codecLabel = context.getString(R.string.codec)
|
val codecLabel = resources.getString(R.string.codec)
|
||||||
val layoutLabel = context.getString(R.string.layout)
|
val layoutLabel = resources.getString(R.string.layout)
|
||||||
val channelsLabel = context.getString(R.string.channels)
|
val channelsLabel = resources.getString(R.string.channels)
|
||||||
val bitrateLabel = context.getString(R.string.bitrate)
|
val bitrateLabel = resources.getString(R.string.bitrate)
|
||||||
val sampleRateLabel = context.getString(R.string.sample_rate)
|
val sampleRateLabel = resources.getString(R.string.sample_rate)
|
||||||
val defaultLabel = context.getString(R.string.default_track)
|
val defaultLabel = resources.getString(R.string.default_track)
|
||||||
val profileLabel = context.getString(R.string.profile)
|
val profileLabel = resources.getString(R.string.profile)
|
||||||
val yesStr = context.getString(R.string.yes)
|
val yesStr = resources.getString(R.string.yes)
|
||||||
val noStr = context.getString(R.string.no)
|
val noStr = resources.getString(R.string.no)
|
||||||
val sampleRateUnit = context.getString(R.string.sample_rate_unit)
|
val sampleRateUnit = resources.getString(R.string.sample_rate_unit)
|
||||||
|
|
||||||
stream.title?.let { add(titleLabel to it) }
|
stream.title?.let { add(titleLabel to it) }
|
||||||
stream.language?.let { add(languageLabel to languageName(it)) }
|
stream.language?.let { add(languageLabel to languageName(it)) }
|
||||||
stream.codec?.let {
|
stream.codec?.let {
|
||||||
val formattedCodec = formatAudioCodec(context, it, stream.profile) + " ($it)"
|
val formattedCodec = formatAudioCodec(resources, it, stream.profile) + " ($it)"
|
||||||
add(codecLabel to formattedCodec)
|
add(codecLabel to formattedCodec)
|
||||||
}
|
}
|
||||||
stream.channelLayout?.let { add(layoutLabel to it) }
|
stream.channelLayout?.let { add(layoutLabel to it) }
|
||||||
|
|
@ -444,20 +444,20 @@ private fun buildAudioStreamInfo(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildSubtitleStreamInfo(
|
private fun buildSubtitleStreamInfo(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
stream: MediaStream,
|
stream: MediaStream,
|
||||||
showPath: Boolean,
|
showPath: Boolean,
|
||||||
): List<Pair<String, String>> =
|
): List<Pair<String, String>> =
|
||||||
buildList {
|
buildList {
|
||||||
val titleLabel = context.getString(R.string.title)
|
val titleLabel = resources.getString(R.string.title)
|
||||||
val languageLabel = context.getString(R.string.language)
|
val languageLabel = resources.getString(R.string.language)
|
||||||
val codecLabel = context.getString(R.string.codec)
|
val codecLabel = resources.getString(R.string.codec)
|
||||||
val defaultLabel = context.getString(R.string.default_track)
|
val defaultLabel = resources.getString(R.string.default_track)
|
||||||
val forcedLabel = context.getString(R.string.forced_track)
|
val forcedLabel = resources.getString(R.string.forced_track)
|
||||||
val externalLabel = context.getString(R.string.external_track)
|
val externalLabel = resources.getString(R.string.external_track)
|
||||||
val yesStr = context.getString(R.string.yes)
|
val yesStr = resources.getString(R.string.yes)
|
||||||
val noStr = context.getString(R.string.no)
|
val noStr = resources.getString(R.string.no)
|
||||||
val pathLabel = context.getString(R.string.path)
|
val pathLabel = resources.getString(R.string.path)
|
||||||
|
|
||||||
stream.title?.let { add(titleLabel to it) }
|
stream.title?.let { add(titleLabel to it) }
|
||||||
stream.language?.let { add(languageLabel to languageName(it)) }
|
stream.language?.let { add(languageLabel to languageName(it)) }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
|
@ -32,7 +32,7 @@ data class MoreDialogActions(
|
||||||
)
|
)
|
||||||
|
|
||||||
fun buildMoreDialogItemsForHome(
|
fun buildMoreDialogItemsForHome(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
seriesId: UUID?,
|
seriesId: UUID?,
|
||||||
playbackPosition: Duration,
|
playbackPosition: Duration,
|
||||||
|
|
@ -47,7 +47,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
val itemId = item.id
|
val itemId = item.id
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to),
|
resources.getString(R.string.go_to),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
) {
|
) {
|
||||||
actions.onClickGoTo(item)
|
actions.onClickGoTo(item)
|
||||||
|
|
@ -57,7 +57,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
if (playbackPosition >= 1.seconds) {
|
if (playbackPosition >= 1.seconds) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.resume),
|
resources.getString(R.string.resume),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -71,7 +71,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.restart),
|
resources.getString(R.string.restart),
|
||||||
Icons.Default.Refresh,
|
Icons.Default.Refresh,
|
||||||
// iconColor = Color.Green.copy(alpha = .8f),
|
// iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -86,7 +86,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
} else {
|
} else {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play),
|
resources.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -103,7 +103,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
if (item.type == BaseItemKind.MUSIC_ALBUM) {
|
if (item.type == BaseItemKind.MUSIC_ALBUM) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.add_to_queue),
|
resources.getString(R.string.add_to_queue),
|
||||||
Icons.Default.Add,
|
Icons.Default.Add,
|
||||||
) {
|
) {
|
||||||
actions.onClickAddToQueue(item)
|
actions.onClickAddToQueue(item)
|
||||||
|
|
@ -121,7 +121,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
if (canDelete) {
|
if (canDelete) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.delete),
|
resources.getString(R.string.delete),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
iconColor = Color.Red.copy(alpha = .8f),
|
iconColor = Color.Red.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -169,7 +169,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
seriesId?.let {
|
seriesId?.let {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.go_to_series),
|
resources.getString(R.string.go_to_series),
|
||||||
Icons.AutoMirrored.Filled.ArrowForward,
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
) {
|
) {
|
||||||
actions.navigateTo(
|
actions.navigateTo(
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playback.scale
|
import com.github.damontecres.wholphin.ui.playback.scale
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringStringProvider
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
|
|
@ -93,13 +95,13 @@ class HomeRowGridViewModel
|
||||||
private val musicService: MusicService,
|
private val musicService: MusicService,
|
||||||
val streamChoiceService: StreamChoiceService,
|
val streamChoiceService: StreamChoiceService,
|
||||||
val mediaReportService: MediaReportService,
|
val mediaReportService: MediaReportService,
|
||||||
@Assisted private val title: String,
|
@Assisted private val title: StringProvider,
|
||||||
@Assisted private val rowConfig: HomeRowConfig,
|
@Assisted private val rowConfig: HomeRowConfig,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
title: String,
|
title: StringProvider,
|
||||||
rowConfig: HomeRowConfig,
|
rowConfig: HomeRowConfig,
|
||||||
): HomeRowGridViewModel
|
): HomeRowGridViewModel
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +157,16 @@ class HomeRowGridViewModel
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error fetching: %s", rowConfig)
|
Timber.e(ex, "Error fetching: %s", rowConfig)
|
||||||
_state.update { it.copy(loading = HomeRowLoadingState.Error(title, null, ex)) }
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
loading =
|
||||||
|
HomeRowLoadingState.Error(
|
||||||
|
title,
|
||||||
|
null,
|
||||||
|
ex,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +221,7 @@ class HomeRowGridViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
data class HomeRowGridState(
|
data class HomeRowGridState(
|
||||||
val loading: HomeRowLoadingState = HomeRowLoadingState.Pending(""),
|
val loading: HomeRowLoadingState = HomeRowLoadingState.Pending(StringStringProvider("")),
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
|
@ -268,7 +279,7 @@ fun HomeRowGrid(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
GridTitle(destination.title)
|
GridTitle(destination.title.getString())
|
||||||
|
|
||||||
when (val st = state.loading) {
|
when (val st = state.loading) {
|
||||||
is HomeRowLoadingState.Error -> {
|
is HomeRowLoadingState.Error -> {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
|
|
@ -385,7 +386,7 @@ fun PersonPageContent(
|
||||||
DiscoverRow(
|
DiscoverRow(
|
||||||
row =
|
row =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
stringResource(R.string.discover),
|
ResStringProvider(R.string.discover),
|
||||||
DataLoadingState.Success(discovered),
|
DataLoadingState.Success(discovered),
|
||||||
DiscoverRequestType.UNKNOWN,
|
DiscoverRequestType.UNKNOWN,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.ui.util.FilterUtils
|
import com.github.damontecres.wholphin.ui.util.FilterUtils
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
|
@ -212,7 +213,7 @@ class CollectionViewModel
|
||||||
val jobs =
|
val jobs =
|
||||||
typesInCollection.map { type ->
|
typesInCollection.map { type ->
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
val title = context.getString(formatTypeName(type))
|
val title = ResStringProvider(formatTypeName(type))
|
||||||
val result =
|
val result =
|
||||||
try {
|
try {
|
||||||
val pager = fetchItems(sort, filter, listOf(type))
|
val pager = fetchItems(sort, filter, listOf(type))
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -79,6 +80,7 @@ fun DiscoverMovieDetails(
|
||||||
creationCallback = { it.create(destination.item) },
|
creationCallback = { it.create(destination.item) },
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
val resources = LocalResources.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LifecycleResumeEffect(Unit) {
|
LifecycleResumeEffect(Unit) {
|
||||||
viewModel.init()
|
viewModel.init()
|
||||||
|
|
@ -160,7 +162,7 @@ fun DiscoverMovieDetails(
|
||||||
overviewOnClick = {
|
overviewOnClick = {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = movie.title ?: context.getString(R.string.unknown),
|
title = movie.title ?: resources.getString(R.string.unknown),
|
||||||
overview = movie.overview,
|
overview = movie.overview,
|
||||||
genres = movie.genres?.mapNotNull { it.name }.orEmpty(),
|
genres = movie.genres?.mapNotNull { it.name }.orEmpty(),
|
||||||
files = listOf(),
|
files = listOf(),
|
||||||
|
|
@ -244,7 +246,6 @@ fun DiscoverMovieDetailsContent(
|
||||||
onLongClickSimilar: (Int, DiscoverItem) -> Unit,
|
onLongClickSimilar: (Int, DiscoverItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var position by rememberInt(0)
|
var position by rememberInt(0)
|
||||||
val focusRequesters = remember { List(RECOMMENDED_ROW + 1) { FocusRequester() } }
|
val focusRequesters = remember { List(RECOMMENDED_ROW + 1) { FocusRequester() } }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.discover
|
package com.github.damontecres.wholphin.ui.detail.discover
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -83,6 +84,7 @@ fun DiscoverSeriesDetails(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
|
||||||
val item by viewModel.tvSeries.observeAsState()
|
val item by viewModel.tvSeries.observeAsState()
|
||||||
|
|
@ -147,7 +149,7 @@ fun DiscoverSeriesDetails(
|
||||||
overviewOnClick = {
|
overviewOnClick = {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = item.name ?: context.getString(R.string.unknown),
|
title = item.name ?: resources.getString(R.string.unknown),
|
||||||
overview = item.overview,
|
overview = item.overview,
|
||||||
genres = item.genres?.mapNotNull { it.name }.orEmpty(),
|
genres = item.genres?.mapNotNull { it.name }.orEmpty(),
|
||||||
files = listOf(),
|
files = listOf(),
|
||||||
|
|
@ -494,7 +496,7 @@ fun DiscoverSeriesDetailsHeader(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildDialogForSeason(
|
fun buildDialogForSeason(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
s: BaseItem,
|
s: BaseItem,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
markPlayed: (Boolean) -> Unit,
|
markPlayed: (Boolean) -> Unit,
|
||||||
|
|
@ -503,26 +505,26 @@ fun buildDialogForSeason(
|
||||||
val items =
|
val items =
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
DialogItem(context.getString(R.string.go_to), Icons.Default.PlayArrow) {
|
DialogItem(resources.getString(R.string.go_to), Icons.Default.PlayArrow) {
|
||||||
onClickItem.invoke(s)
|
onClickItem.invoke(s)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (s.data.userData?.played == true) {
|
if (s.data.userData?.played == true) {
|
||||||
add(
|
add(
|
||||||
DialogItem(context.getString(R.string.mark_unwatched), R.string.fa_eye) {
|
DialogItem(resources.getString(R.string.mark_unwatched), R.string.fa_eye) {
|
||||||
markPlayed.invoke(false)
|
markPlayed.invoke(false)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
add(
|
add(
|
||||||
DialogItem(context.getString(R.string.mark_watched), R.string.fa_eye_slash) {
|
DialogItem(resources.getString(R.string.mark_watched), R.string.fa_eye_slash) {
|
||||||
markPlayed.invoke(true)
|
markPlayed.invoke(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play),
|
resources.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -531,7 +533,7 @@ fun buildDialogForSeason(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.shuffle),
|
resources.getString(R.string.shuffle),
|
||||||
R.string.fa_shuffle,
|
R.string.fa_shuffle,
|
||||||
) {
|
) {
|
||||||
onClickPlay.invoke(true)
|
onClickPlay.invoke(true)
|
||||||
|
|
@ -539,7 +541,7 @@ fun buildDialogForSeason(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return DialogParams(
|
return DialogParams(
|
||||||
title = s.name ?: context.getString(R.string.tv_season),
|
title = s.name ?: resources.getString(R.string.tv_season),
|
||||||
fromLongClick = true,
|
fromLongClick = true,
|
||||||
items = items,
|
items = items,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import com.github.damontecres.wholphin.ui.discover.DiscoverRowData
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -478,7 +479,7 @@ fun MovieDetailsContent(
|
||||||
DiscoverRow(
|
DiscoverRow(
|
||||||
row =
|
row =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
stringResource(R.string.discover),
|
ResStringProvider(R.string.discover),
|
||||||
DataLoadingState.Success(discovered),
|
DataLoadingState.Success(discovered),
|
||||||
type = DiscoverRequestType.UNKNOWN,
|
type = DiscoverRequestType.UNKNOWN,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.series
|
package com.github.damontecres.wholphin.ui.detail.series
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
|
@ -85,6 +85,7 @@ import com.github.damontecres.wholphin.ui.discover.DiscoverRowData
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -644,7 +645,7 @@ fun SeriesDetailsContent(
|
||||||
DiscoverRow(
|
DiscoverRow(
|
||||||
row =
|
row =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
stringResource(R.string.discover),
|
ResStringProvider(R.string.discover),
|
||||||
DataLoadingState.Success(discovered),
|
DataLoadingState.Success(discovered),
|
||||||
type = DiscoverRequestType.UNKNOWN,
|
type = DiscoverRequestType.UNKNOWN,
|
||||||
),
|
),
|
||||||
|
|
@ -726,7 +727,7 @@ fun SeriesDetailsHeader(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildDialogForSeason(
|
fun buildDialogForSeason(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
s: BaseItem,
|
s: BaseItem,
|
||||||
canDelete: Boolean,
|
canDelete: Boolean,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
|
|
@ -737,26 +738,26 @@ fun buildDialogForSeason(
|
||||||
val items =
|
val items =
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
DialogItem(context.getString(R.string.go_to), Icons.Default.PlayArrow) {
|
DialogItem(resources.getString(R.string.go_to), Icons.Default.PlayArrow) {
|
||||||
onClickItem.invoke(s)
|
onClickItem.invoke(s)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (s.data.userData?.played == true) {
|
if (s.data.userData?.played == true) {
|
||||||
add(
|
add(
|
||||||
DialogItem(context.getString(R.string.mark_unwatched), R.string.fa_eye) {
|
DialogItem(resources.getString(R.string.mark_unwatched), R.string.fa_eye) {
|
||||||
markPlayed.invoke(false)
|
markPlayed.invoke(false)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
add(
|
add(
|
||||||
DialogItem(context.getString(R.string.mark_watched), R.string.fa_eye_slash) {
|
DialogItem(resources.getString(R.string.mark_watched), R.string.fa_eye_slash) {
|
||||||
markPlayed.invoke(true)
|
markPlayed.invoke(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play),
|
resources.getString(R.string.play),
|
||||||
Icons.Default.PlayArrow,
|
Icons.Default.PlayArrow,
|
||||||
iconColor = Color.Green.copy(alpha = .8f),
|
iconColor = Color.Green.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -765,7 +766,7 @@ fun buildDialogForSeason(
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.shuffle),
|
resources.getString(R.string.shuffle),
|
||||||
R.string.fa_shuffle,
|
R.string.fa_shuffle,
|
||||||
) {
|
) {
|
||||||
onClickPlay.invoke(true)
|
onClickPlay.invoke(true)
|
||||||
|
|
@ -774,7 +775,7 @@ fun buildDialogForSeason(
|
||||||
if (canDelete) {
|
if (canDelete) {
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.delete),
|
resources.getString(R.string.delete),
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
iconColor = Color.Red.copy(alpha = .8f),
|
iconColor = Color.Red.copy(alpha = .8f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -784,7 +785,7 @@ fun buildDialogForSeason(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DialogParams(
|
return DialogParams(
|
||||||
title = s.name ?: context.getString(R.string.tv_season),
|
title = s.name ?: resources.getString(R.string.tv_season),
|
||||||
fromLongClick = true,
|
fromLongClick = true,
|
||||||
items = items,
|
items = items,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ fun DiscoverRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = row.title,
|
text = row.title.getString(),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
|
|
@ -73,7 +73,7 @@ fun DiscoverRow(
|
||||||
|
|
||||||
is DataLoadingState.Success<List<DiscoverItem>> -> {
|
is DataLoadingState.Success<List<DiscoverItem>> -> {
|
||||||
DiscoverItemRow(
|
DiscoverItemRow(
|
||||||
title = row.title,
|
title = row.title.getString(),
|
||||||
items = state.data,
|
items = state.data,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,10 @@ import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.ui.util.EmptyStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
import com.google.common.cache.CacheBuilder
|
import com.google.common.cache.CacheBuilder
|
||||||
|
|
@ -77,7 +80,7 @@ class SeerrDiscoverViewModel
|
||||||
this.copy(
|
this.copy(
|
||||||
movies =
|
movies =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
context.getString(R.string.movies),
|
ResStringProvider(R.string.movies),
|
||||||
it,
|
it,
|
||||||
DiscoverRequestType.DISCOVER_MOVIES,
|
DiscoverRequestType.DISCOVER_MOVIES,
|
||||||
),
|
),
|
||||||
|
|
@ -87,7 +90,7 @@ class SeerrDiscoverViewModel
|
||||||
this.copy(
|
this.copy(
|
||||||
tv =
|
tv =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
context.getString(R.string.tv_shows),
|
ResStringProvider(R.string.tv_shows),
|
||||||
it,
|
it,
|
||||||
DiscoverRequestType.DISCOVER_TV,
|
DiscoverRequestType.DISCOVER_TV,
|
||||||
),
|
),
|
||||||
|
|
@ -97,7 +100,7 @@ class SeerrDiscoverViewModel
|
||||||
this.copy(
|
this.copy(
|
||||||
trending =
|
trending =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
context.getString(R.string.trending),
|
ResStringProvider(R.string.trending),
|
||||||
it,
|
it,
|
||||||
DiscoverRequestType.TRENDING,
|
DiscoverRequestType.TRENDING,
|
||||||
),
|
),
|
||||||
|
|
@ -107,7 +110,7 @@ class SeerrDiscoverViewModel
|
||||||
this.copy(
|
this.copy(
|
||||||
upcomingMovies =
|
upcomingMovies =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
context.getString(R.string.upcoming_movies),
|
ResStringProvider(R.string.upcoming_movies),
|
||||||
it,
|
it,
|
||||||
DiscoverRequestType.UPCOMING_MOVIES,
|
DiscoverRequestType.UPCOMING_MOVIES,
|
||||||
),
|
),
|
||||||
|
|
@ -117,7 +120,7 @@ class SeerrDiscoverViewModel
|
||||||
this.copy(
|
this.copy(
|
||||||
upcomingTv =
|
upcomingTv =
|
||||||
DiscoverRowData(
|
DiscoverRowData(
|
||||||
context.getString(R.string.upcoming_tv),
|
ResStringProvider(R.string.upcoming_tv),
|
||||||
it,
|
it,
|
||||||
DiscoverRequestType.UPCOMING_TV,
|
DiscoverRequestType.UPCOMING_TV,
|
||||||
),
|
),
|
||||||
|
|
@ -214,12 +217,17 @@ class SeerrDiscoverViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DiscoverRowData(
|
data class DiscoverRowData(
|
||||||
val title: String,
|
val title: StringProvider,
|
||||||
val items: DataLoadingState<List<DiscoverItem>>,
|
val items: DataLoadingState<List<DiscoverItem>>,
|
||||||
val type: DiscoverRequestType,
|
val type: DiscoverRequestType,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = DiscoverRowData("", DataLoadingState.Pending, DiscoverRequestType.UNKNOWN)
|
val EMPTY =
|
||||||
|
DiscoverRowData(
|
||||||
|
EmptyStringProvider,
|
||||||
|
DataLoadingState.Pending,
|
||||||
|
DiscoverRequestType.UNKNOWN,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,7 @@ fun HomePageContent(
|
||||||
is HomeRowLoadingState.Pending,
|
is HomeRowLoadingState.Pending,
|
||||||
-> {
|
-> {
|
||||||
FocusableItemRow(
|
FocusableItemRow(
|
||||||
title = r.title,
|
title = r.title.getString(),
|
||||||
subtitle = stringResource(R.string.loading),
|
subtitle = stringResource(R.string.loading),
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
)
|
)
|
||||||
|
|
@ -389,7 +389,7 @@ fun HomePageContent(
|
||||||
|
|
||||||
is HomeRowLoadingState.Error -> {
|
is HomeRowLoadingState.Error -> {
|
||||||
FocusableItemRow(
|
FocusableItemRow(
|
||||||
title = r.title,
|
title = r.title.getString(),
|
||||||
subtitle = r.localizedMessage,
|
subtitle = r.localizedMessage,
|
||||||
isError = true,
|
isError = true,
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
|
|
@ -400,7 +400,7 @@ fun HomePageContent(
|
||||||
if (row.items.isNotEmpty()) {
|
if (row.items.isNotEmpty()) {
|
||||||
val viewOptions = row.viewOptions
|
val viewOptions = row.viewOptions
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = row.title,
|
title = row.title.getString(),
|
||||||
items = row.items,
|
items = row.items,
|
||||||
onClickItem =
|
onClickItem =
|
||||||
remember(rowIndex, onClickItem) {
|
remember(rowIndex, onClickItem) {
|
||||||
|
|
@ -502,7 +502,7 @@ fun HomePageContent(
|
||||||
)
|
)
|
||||||
} else if (showEmptyRows) {
|
} else if (showEmptyRows) {
|
||||||
FocusableItemRow(
|
FocusableItemRow(
|
||||||
title = r.title,
|
title = r.title.getString(),
|
||||||
subtitle = stringResource(R.string.no_results),
|
subtitle = stringResource(R.string.no_results),
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
|
import com.github.damontecres.wholphin.ui.util.EmptyStringProvider
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -103,7 +104,9 @@ class HomeViewModel
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
it.homeRows
|
it.homeRows
|
||||||
} else {
|
} else {
|
||||||
List(settings.rows.size) { HomeRowLoadingState.Pending("") }
|
List(settings.rows.size) {
|
||||||
|
HomeRowLoadingState.Pending(EmptyStringProvider)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ fun HomeSettingsPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HomeRowSettings(
|
HomeRowSettings(
|
||||||
title = row.title,
|
title = row.title.getString(),
|
||||||
preferenceOptions = preferenceOptions,
|
preferenceOptions = preferenceOptions,
|
||||||
viewOptions = row.config.viewOptions,
|
viewOptions = row.config.viewOptions,
|
||||||
defaultViewOptions = defaultViewOptions,
|
defaultViewOptions = defaultViewOptions,
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ fun HomeRowConfigContent(
|
||||||
) {
|
) {
|
||||||
HomeSettingsListItem(
|
HomeSettingsListItem(
|
||||||
selected = false,
|
selected = false,
|
||||||
headlineText = config.title,
|
headlineText = config.title.getString(),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ import com.github.damontecres.wholphin.services.tvAccess
|
||||||
import com.github.damontecres.wholphin.ui.AspectRatio
|
import com.github.damontecres.wholphin.ui.AspectRatio
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResArgStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResProviderStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ResStringProvider
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringStringProvider
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -222,7 +226,7 @@ class HomeSettingsViewModel
|
||||||
MetaRowType.CONTINUE_WATCHING -> {
|
MetaRowType.CONTINUE_WATCHING -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = context.getString(R.string.continue_watching),
|
title = ResStringProvider(R.string.continue_watching),
|
||||||
config = ContinueWatching(),
|
config = ContinueWatching(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +234,7 @@ class HomeSettingsViewModel
|
||||||
MetaRowType.NEXT_UP -> {
|
MetaRowType.NEXT_UP -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = context.getString(R.string.next_up),
|
title = ResStringProvider(R.string.next_up),
|
||||||
config = NextUp(),
|
config = NextUp(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +242,7 @@ class HomeSettingsViewModel
|
||||||
MetaRowType.COMBINED_CONTINUE_WATCHING -> {
|
MetaRowType.COMBINED_CONTINUE_WATCHING -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = context.getString(R.string.combine_continue_next),
|
title = ResStringProvider(R.string.combine_continue_next),
|
||||||
config = ContinueWatchingCombined(),
|
config = ContinueWatchingCombined(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -291,7 +295,12 @@ class HomeSettingsViewModel
|
||||||
when (rowType) {
|
when (rowType) {
|
||||||
LibraryRowType.RECENTLY_ADDED -> {
|
LibraryRowType.RECENTLY_ADDED -> {
|
||||||
val title =
|
val title =
|
||||||
library.name.let { context.getString(R.string.recently_added_in, it) }
|
library.name.let {
|
||||||
|
ResArgStringProvider(
|
||||||
|
R.string.recently_added_in,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
}
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -302,7 +311,7 @@ class HomeSettingsViewModel
|
||||||
LibraryRowType.RECENTLY_RELEASED -> {
|
LibraryRowType.RECENTLY_RELEASED -> {
|
||||||
val title =
|
val title =
|
||||||
library.name.let {
|
library.name.let {
|
||||||
context.getString(
|
ResArgStringProvider(
|
||||||
R.string.recently_released_in,
|
R.string.recently_released_in,
|
||||||
it,
|
it,
|
||||||
)
|
)
|
||||||
|
|
@ -315,7 +324,8 @@ class HomeSettingsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryRowType.GENRES -> {
|
LibraryRowType.GENRES -> {
|
||||||
val title = library.name.let { context.getString(R.string.genres_in, it) }
|
val title =
|
||||||
|
library.name.let { ResArgStringProvider(R.string.genres_in, it) }
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -325,7 +335,7 @@ class HomeSettingsViewModel
|
||||||
|
|
||||||
LibraryRowType.STUDIOS -> {
|
LibraryRowType.STUDIOS -> {
|
||||||
val title =
|
val title =
|
||||||
library.name.let { context.getString(R.string.studios_in, it) }
|
library.name.let { ResArgStringProvider(R.string.studios_in, it) }
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -335,7 +345,12 @@ class HomeSettingsViewModel
|
||||||
|
|
||||||
LibraryRowType.SUGGESTIONS -> {
|
LibraryRowType.SUGGESTIONS -> {
|
||||||
val title =
|
val title =
|
||||||
library.name.let { context.getString(R.string.suggestions_for, it) }
|
library.name.let {
|
||||||
|
ResArgStringProvider(
|
||||||
|
R.string.suggestions_for,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
}
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -344,7 +359,7 @@ class HomeSettingsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryRowType.TV_CHANNELS -> {
|
LibraryRowType.TV_CHANNELS -> {
|
||||||
val title = context.getString(R.string.channels)
|
val title = ResStringProvider(R.string.channels)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -356,7 +371,7 @@ class HomeSettingsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryRowType.TV_PROGRAMS -> {
|
LibraryRowType.TV_PROGRAMS -> {
|
||||||
val title = context.getString(R.string.watch_live)
|
val title = ResStringProvider(R.string.watch_live)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -365,7 +380,7 @@ class HomeSettingsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryRowType.RECENTLY_RECORDED -> {
|
LibraryRowType.RECENTLY_RECORDED -> {
|
||||||
val title = context.getString(R.string.recently_recorded)
|
val title = ResStringProvider(R.string.recently_recorded)
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -396,9 +411,9 @@ class HomeSettingsViewModel
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title =
|
title =
|
||||||
context.getString(
|
ResProviderStringProvider(
|
||||||
R.string.favorite_items,
|
R.string.favorite_items,
|
||||||
context.getString(favoriteOptions[type]!!),
|
ResStringProvider(favoriteOptions[type]!!),
|
||||||
),
|
),
|
||||||
config = HomeRowConfig.Favorite(type),
|
config = HomeRowConfig.Favorite(type),
|
||||||
)
|
)
|
||||||
|
|
@ -420,7 +435,7 @@ class HomeSettingsViewModel
|
||||||
val newRow =
|
val newRow =
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = parent.name ?: "",
|
title = StringStringProvider(parent.name ?: ""),
|
||||||
config =
|
config =
|
||||||
HomeRowConfig.ByParent(
|
HomeRowConfig.ByParent(
|
||||||
parentId = parent.id,
|
parentId = parent.id,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
package com.github.damontecres.wholphin.ui.nav
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
|
|
@ -14,6 +13,7 @@ import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
import com.github.damontecres.wholphin.util.DiscoverRequestType
|
||||||
import com.github.damontecres.wholphin.util.RequestHandler
|
import com.github.damontecres.wholphin.util.RequestHandler
|
||||||
import com.github.damontecres.wholphin.util.SEERR_PAGE_SIZE
|
import com.github.damontecres.wholphin.util.SEERR_PAGE_SIZE
|
||||||
|
|
@ -110,8 +110,7 @@ sealed class Destination(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ItemGrid<T>(
|
data class ItemGrid<T>(
|
||||||
val title: String?,
|
val title: StringProvider,
|
||||||
@param:StringRes val titleRes: Int?,
|
|
||||||
@Contextual val request: T,
|
@Contextual val request: T,
|
||||||
val requestHandler: RequestHandler<T>,
|
val requestHandler: RequestHandler<T>,
|
||||||
val initialPosition: Int = 0,
|
val initialPosition: Int = 0,
|
||||||
|
|
@ -120,7 +119,7 @@ sealed class Destination(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MoreHomeRow(
|
data class MoreHomeRow(
|
||||||
val title: String,
|
val title: StringProvider,
|
||||||
val config: HomeRowConfig,
|
val config: HomeRowConfig,
|
||||||
val initialPosition: Int,
|
val initialPosition: Int,
|
||||||
) : Destination(false)
|
) : Destination(false)
|
||||||
|
|
|
||||||
|
|
@ -509,14 +509,15 @@ class PlaybackViewModel
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
}?.map {
|
}?.map {
|
||||||
SimpleMediaStream.from(context, it, true)
|
// TODO should use a string provider instead
|
||||||
|
SimpleMediaStream.from(context.resources, it, true)
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
|
|
||||||
val audioStreams =
|
val audioStreams =
|
||||||
mediaSource.mediaStreams
|
mediaSource.mediaStreams
|
||||||
?.filter { it.type == MediaStreamType.AUDIO }
|
?.filter { it.type == MediaStreamType.AUDIO }
|
||||||
?.map {
|
?.map {
|
||||||
SimpleMediaStream.from(context, it, true)
|
SimpleMediaStream.from(context.resources, it, true)
|
||||||
}
|
}
|
||||||
// ?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
|
// ?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.playback
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.util.StreamFormatting.mediaStreamDisplayTitle
|
import com.github.damontecres.wholphin.ui.util.StreamFormatting.mediaStreamDisplayTitle
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
|
|
@ -17,14 +17,14 @@ data class SimpleMediaStream(
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun from(
|
fun from(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
mediaStream: MediaStream,
|
mediaStream: MediaStream,
|
||||||
includeFlags: Boolean = true,
|
includeFlags: Boolean = true,
|
||||||
): SimpleMediaStream =
|
): SimpleMediaStream =
|
||||||
SimpleMediaStream(
|
SimpleMediaStream(
|
||||||
index = mediaStream.index,
|
index = mediaStream.index,
|
||||||
streamTitle = mediaStream.title?.takeIf { it.isNotNullOrBlank() },
|
streamTitle = mediaStream.title?.takeIf { it.isNotNullOrBlank() },
|
||||||
displayTitle = mediaStreamDisplayTitle(context, mediaStream, includeFlags),
|
displayTitle = mediaStreamDisplayTitle(resources, mediaStream, includeFlags),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
|
||||||
it.copy(
|
it.copy(
|
||||||
subtitleStreams =
|
subtitleStreams =
|
||||||
subtitlesStreams.map {
|
subtitlesStreams.map {
|
||||||
SimpleMediaStream.from(context, it, true)
|
SimpleMediaStream.from(context.resources, it, true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.XmlResourceParser
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||||
|
import com.github.damontecres.wholphin.ui.components.DialogPopupContent
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||||
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class LocaleChoiceViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
fun changeLocale(locale: Locale) {
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
serverRepository.current.value?.let {
|
||||||
|
Timber.i("Updating user's locale to %s", locale.toLanguageTag())
|
||||||
|
val newUser = it.user.copy(uiLanguage = locale.toLanguageTag())
|
||||||
|
serverRepository.changeUser(it.server, newUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(LocaleChoiceState())
|
||||||
|
val state: StateFlow<LocaleChoiceState> = _state
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
serverRepository.currentUser.value?.let {
|
||||||
|
val availableLocales = extractAvailableLocales()
|
||||||
|
Timber.v("availableLocales=%s", availableLocales)
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
loading = LoadingState.Success,
|
||||||
|
availableLocales = availableLocales,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
serverRepository.currentUser.asFlow().collectLatest { user ->
|
||||||
|
val userLocale = user?.uiLanguage?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
||||||
|
_state.update { it.copy(userLocale = userLocale) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the available locales that can be used.
|
||||||
|
*
|
||||||
|
* Defaults to `context.assets.locales` (all locales) if can't find app specific ones
|
||||||
|
*/
|
||||||
|
private fun extractAvailableLocales(): List<Locale> {
|
||||||
|
val id =
|
||||||
|
context.resources.getIdentifier(
|
||||||
|
"_generated_res_locale_config",
|
||||||
|
"xml",
|
||||||
|
context.packageName,
|
||||||
|
)
|
||||||
|
return if (id != 0) {
|
||||||
|
try {
|
||||||
|
val locales = mutableListOf<Locale>()
|
||||||
|
// This is kind of a hack, the generated locale config is not available,
|
||||||
|
// programmatically, so this code parses the raw XML
|
||||||
|
context.resources.getXml(id).use { parser ->
|
||||||
|
var eventType: Int = parser.eventType
|
||||||
|
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||||
|
if (eventType == XmlResourceParser.START_TAG) {
|
||||||
|
val tagName: String? = parser.name
|
||||||
|
|
||||||
|
// Check for a specific tag name
|
||||||
|
if ("locale" == tagName) {
|
||||||
|
val attr =
|
||||||
|
parser.getAttributeValue(
|
||||||
|
"http://schemas.android.com/apk/res/android",
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
attr?.let { locales.add(Locale.forLanguageTag(it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventType = parser.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
locales
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Exception while parsing generated available locales")
|
||||||
|
context.assets.locales.map { Locale.forLanguageTag(it) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.assets.locales.map { Locale.forLanguageTag(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LocaleChoiceState(
|
||||||
|
val loading: LoadingState = LoadingState.Pending,
|
||||||
|
val availableLocales: List<Locale> = emptyList(),
|
||||||
|
val userLocale: Locale = Locale.getDefault(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LocaleChoiceDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: LocaleChoiceViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = DialogProperties(),
|
||||||
|
) {
|
||||||
|
when (val st = state.loading) {
|
||||||
|
is LoadingState.Error -> {
|
||||||
|
ErrorMessage(st)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
LoadingPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
val dialogItems =
|
||||||
|
remember(state.userLocale, state.availableLocales) {
|
||||||
|
val userLanguage = state.userLocale.toLanguageTag()
|
||||||
|
state.availableLocales.map { locale ->
|
||||||
|
val tag = locale.toLanguageTag()
|
||||||
|
DialogItem(
|
||||||
|
selected = tag == userLanguage,
|
||||||
|
leadingContent = {
|
||||||
|
Box {
|
||||||
|
SelectedLeadingContent(tag == userLanguage)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(locale.getDisplayName(locale))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(locale.getDisplayName(Locale.ENGLISH))
|
||||||
|
},
|
||||||
|
dismissOnClick = true,
|
||||||
|
onClick = {
|
||||||
|
viewModel.changeLocale(locale)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DialogPopupContent(
|
||||||
|
title = stringResource(R.string.user_interface_language),
|
||||||
|
dialogItems = dialogItems,
|
||||||
|
waiting = false,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -82,6 +82,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PreferencesContent(
|
fun PreferencesContent(
|
||||||
|
|
@ -109,6 +110,7 @@ fun PreferencesContent(
|
||||||
val seerrConnection by viewModel.seerrConnection.collectAsState()
|
val seerrConnection by viewModel.seerrConnection.collectAsState()
|
||||||
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
|
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
|
||||||
var showQuickConnectDialog by remember { mutableStateOf(false) }
|
var showQuickConnectDialog by remember { mutableStateOf(false) }
|
||||||
|
var showLocaleChoiceDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.preferenceDataStore.data.collect {
|
viewModel.preferenceDataStore.data.collect {
|
||||||
|
|
@ -255,6 +257,12 @@ fun PreferencesContent(
|
||||||
pref as AppPreference<AppPreferences, Any>
|
pref as AppPreference<AppPreferences, Any>
|
||||||
item {
|
item {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val focusModifier =
|
||||||
|
Modifier
|
||||||
|
.ifElse(
|
||||||
|
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||||
|
Modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
val focused = interactionSource.collectIsFocusedAsState().value
|
val focused = interactionSource.collectIsFocusedAsState().value
|
||||||
LaunchedEffect(focused) {
|
LaunchedEffect(focused) {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
|
|
@ -275,12 +283,7 @@ fun PreferencesContent(
|
||||||
},
|
},
|
||||||
summary = installedVersion.toString(),
|
summary = installedVersion.toString(),
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
modifier =
|
modifier = focusModifier,
|
||||||
Modifier
|
|
||||||
.ifElse(
|
|
||||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
|
||||||
Modifier.focusRequester(focusRequester),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,12 +320,7 @@ fun PreferencesContent(
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
modifier =
|
modifier = focusModifier,
|
||||||
Modifier
|
|
||||||
.ifElse(
|
|
||||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
|
||||||
Modifier.focusRequester(focusRequester),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,7 +346,7 @@ fun PreferencesContent(
|
||||||
updateCache = true
|
updateCache = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = focusModifier,
|
||||||
summary = summary,
|
summary = summary,
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -359,7 +357,7 @@ fun PreferencesContent(
|
||||||
NavDrawerPreference(
|
NavDrawerPreference(
|
||||||
title = stringResource(pref.title),
|
title = stringResource(pref.title),
|
||||||
summary = pref.summary(context, null),
|
summary = pref.summary(context, null),
|
||||||
modifier = Modifier,
|
modifier = focusModifier,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -370,7 +368,7 @@ fun PreferencesContent(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.sendAppLogs()
|
viewModel.sendAppLogs()
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = focusModifier,
|
||||||
summary = pref.summary(context, null),
|
summary = pref.summary(context, null),
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -383,7 +381,7 @@ fun PreferencesContent(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.resetSubtitleSettings()
|
viewModel.resetSubtitleSettings()
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = focusModifier,
|
||||||
summary = pref.summary(context, null),
|
summary = pref.summary(context, null),
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -400,6 +398,7 @@ fun PreferencesContent(
|
||||||
ChoicePreference(
|
ChoicePreference(
|
||||||
title = stringResource(pref.title),
|
title = stringResource(pref.title),
|
||||||
summary = stringResource(summary),
|
summary = stringResource(summary),
|
||||||
|
interactionSource = interactionSource,
|
||||||
possibleValues =
|
possibleValues =
|
||||||
listOf(
|
listOf(
|
||||||
stringResource(R.string.none),
|
stringResource(R.string.none),
|
||||||
|
|
@ -429,6 +428,7 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
modifier = focusModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,7 +456,7 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = focusModifier,
|
||||||
summary =
|
summary =
|
||||||
when (seerrConnection) {
|
when (seerrConnection) {
|
||||||
is SeerrConnectionStatus.Error -> {
|
is SeerrConnectionStatus.Error -> {
|
||||||
|
|
@ -487,7 +487,7 @@ fun PreferencesContent(
|
||||||
showQuickConnectDialog = true
|
showQuickConnectDialog = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = focusModifier,
|
||||||
summary = pref.summary(context, null),
|
summary = pref.summary(context, null),
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -500,7 +500,7 @@ fun PreferencesContent(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.screensaverService.start()
|
viewModel.screensaverService.start()
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = focusModifier,
|
||||||
summary = pref.summary(context, null),
|
summary = pref.summary(context, null),
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -516,6 +516,7 @@ fun PreferencesContent(
|
||||||
ChoicePreference(
|
ChoicePreference(
|
||||||
title = stringResource(pref.title),
|
title = stringResource(pref.title),
|
||||||
summary = players[selectedIndex].name,
|
summary = players[selectedIndex].name,
|
||||||
|
interactionSource = interactionSource,
|
||||||
possibleValues = players,
|
possibleValues = players,
|
||||||
selectedIndex = selectedIndex,
|
selectedIndex = selectedIndex,
|
||||||
onValueChange = { index ->
|
onValueChange = { index ->
|
||||||
|
|
@ -543,6 +544,25 @@ fun PreferencesContent(
|
||||||
Text(item.name)
|
Text(item.name)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
modifier = focusModifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppPreference.UserInterfaceLanguage -> {
|
||||||
|
val locale =
|
||||||
|
remember(currentUser?.uiLanguage) {
|
||||||
|
currentUser?.uiLanguage?.let { Locale.forLanguageTag(it) }
|
||||||
|
?: Locale.getDefault()
|
||||||
|
}
|
||||||
|
ClickPreference(
|
||||||
|
title = stringResource(pref.title),
|
||||||
|
onClick = {
|
||||||
|
showLocaleChoiceDialog = true
|
||||||
|
},
|
||||||
|
modifier = focusModifier,
|
||||||
|
summary = locale.getDisplayName(locale),
|
||||||
|
onLongClick = null,
|
||||||
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -576,12 +596,7 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
modifier =
|
modifier = focusModifier,
|
||||||
Modifier
|
|
||||||
.ifElse(
|
|
||||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
|
||||||
Modifier.focusRequester(focusRequester),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -721,6 +736,11 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showLocaleChoiceDialog) {
|
||||||
|
LocaleChoiceDialog(
|
||||||
|
onDismissRequest = { showLocaleChoiceDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.util
|
package com.github.damontecres.wholphin.ui.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.util.languageName
|
import com.github.damontecres.wholphin.util.languageName
|
||||||
|
|
@ -44,7 +44,7 @@ object StreamFormatting {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatVideoRange(
|
fun formatVideoRange(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
videoRange: VideoRange?,
|
videoRange: VideoRange?,
|
||||||
type: VideoRangeType?,
|
type: VideoRangeType?,
|
||||||
doviTitle: String?,
|
doviTitle: String?,
|
||||||
|
|
@ -58,7 +58,7 @@ object StreamFormatting {
|
||||||
|
|
||||||
VideoRange.HDR -> {
|
VideoRange.HDR -> {
|
||||||
if (doviTitle.isNotNullOrBlank()) {
|
if (doviTitle.isNotNullOrBlank()) {
|
||||||
context.getString(R.string.dolby_vision)
|
resources.getString(R.string.dolby_vision)
|
||||||
} else {
|
} else {
|
||||||
when (type) {
|
when (type) {
|
||||||
VideoRangeType.UNKNOWN,
|
VideoRangeType.UNKNOWN,
|
||||||
|
|
@ -76,40 +76,40 @@ object StreamFormatting {
|
||||||
VideoRangeType.DOVI_WITH_HDR10,
|
VideoRangeType.DOVI_WITH_HDR10,
|
||||||
VideoRangeType.DOVI_WITH_HLG,
|
VideoRangeType.DOVI_WITH_HLG,
|
||||||
VideoRangeType.DOVI_WITH_SDR,
|
VideoRangeType.DOVI_WITH_SDR,
|
||||||
-> context.getString(R.string.dolby_vision)
|
-> resources.getString(R.string.dolby_vision)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatAudioCodec(
|
fun formatAudioCodec(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
codec: String?,
|
codec: String?,
|
||||||
profile: String?,
|
profile: String?,
|
||||||
): String? =
|
): String? =
|
||||||
when {
|
when {
|
||||||
profile?.contains("Dolby Atmos", true) == true -> {
|
profile?.contains("Dolby Atmos", true) == true -> {
|
||||||
context.getString(R.string.dolby_atmos)
|
resources.getString(R.string.dolby_atmos)
|
||||||
}
|
}
|
||||||
|
|
||||||
profile?.contains("DTS", true) == true -> {
|
profile?.contains("DTS", true) == true -> {
|
||||||
when {
|
when {
|
||||||
profile.contains("X", true) -> context.getString(R.string.dts_x)
|
profile.contains("X", true) -> resources.getString(R.string.dts_x)
|
||||||
profile.contains("MA", true) -> context.getString(R.string.dts_hd_ma)
|
profile.contains("MA", true) -> resources.getString(R.string.dts_hd_ma)
|
||||||
profile.contains("HD", true) -> context.getString(R.string.dts_hd)
|
profile.contains("HD", true) -> resources.getString(R.string.dts_hd)
|
||||||
else -> context.getString(R.string.dts)
|
else -> resources.getString(R.string.dts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
when (codec?.lowercase()) {
|
when (codec?.lowercase()) {
|
||||||
Codec.Audio.TRUEHD -> context.getString(R.string.truehd)
|
Codec.Audio.TRUEHD -> resources.getString(R.string.truehd)
|
||||||
|
|
||||||
Codec.Audio.AC3 -> context.getString(R.string.dolby_digital)
|
Codec.Audio.AC3 -> resources.getString(R.string.dolby_digital)
|
||||||
|
|
||||||
Codec.Audio.EAC3 -> context.getString(R.string.dolby_digital_plus)
|
Codec.Audio.EAC3 -> resources.getString(R.string.dolby_digital_plus)
|
||||||
|
|
||||||
Codec.Audio.DCA -> context.getString(R.string.dts)
|
Codec.Audio.DCA -> resources.getString(R.string.dts)
|
||||||
|
|
||||||
Codec.Audio.OGG,
|
Codec.Audio.OGG,
|
||||||
Codec.Audio.OPUS,
|
Codec.Audio.OPUS,
|
||||||
|
|
@ -141,7 +141,7 @@ object StreamFormatting {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mediaStreamDisplayTitle(
|
fun mediaStreamDisplayTitle(
|
||||||
context: Context,
|
resources: Resources,
|
||||||
stream: MediaStream,
|
stream: MediaStream,
|
||||||
includeFlags: Boolean,
|
includeFlags: Boolean,
|
||||||
): String {
|
): String {
|
||||||
|
|
@ -149,7 +149,7 @@ object StreamFormatting {
|
||||||
buildList {
|
buildList {
|
||||||
add(languageName(stream.language))
|
add(languageName(stream.language))
|
||||||
if (stream.type == MediaStreamType.AUDIO) {
|
if (stream.type == MediaStreamType.AUDIO) {
|
||||||
add(formatAudioCodec(context, stream.codec, stream.profile))
|
add(formatAudioCodec(resources, stream.codec, stream.profile))
|
||||||
add(stream.channelLayout)
|
add(stream.channelLayout)
|
||||||
} else if (stream.type == MediaStreamType.SUBTITLE) {
|
} else if (stream.type == MediaStreamType.SUBTITLE) {
|
||||||
"SDH".takeIf { stream.isHearingImpaired }?.let(::add)
|
"SDH".takeIf { stream.isHearingImpaired }?.let(::add)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.util
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flexible way to provide a string for the UI
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
sealed interface StringProvider {
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun getString(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a string literal
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class StringStringProvider(
|
||||||
|
val str: String,
|
||||||
|
) : StringProvider {
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
override fun getString(): String = str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide an empty string literal
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data object EmptyStringProvider : StringProvider {
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
override fun getString(): String = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a string resource using [stringResource]
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ResStringProvider(
|
||||||
|
@param:StringRes val stringResId: Int,
|
||||||
|
) : StringProvider {
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
override fun getString() = stringResource(stringResId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a string resource with format arguments using [stringResource]
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ResArgStringProvider(
|
||||||
|
@param:StringRes val stringResId: Int,
|
||||||
|
val args: Array<String>,
|
||||||
|
) : StringProvider {
|
||||||
|
constructor(stringResId: Int, arg: String) : this(stringResId, arrayOf(arg))
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
override fun getString() = stringResource(stringResId, *args)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is ResArgStringProvider) return false
|
||||||
|
|
||||||
|
if (stringResId != other.stringResId) return false
|
||||||
|
if (!args.contentEquals(other.args)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = stringResId
|
||||||
|
result = 31 * result + args.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a string resource with another [StringProvider] format argument using [stringResource]
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ResProviderStringProvider(
|
||||||
|
@param:StringRes val stringResId: Int,
|
||||||
|
val argProvider: StringProvider,
|
||||||
|
) : StringProvider {
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
override fun getString(): String = stringResource(stringResId, argProvider.getString())
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.util
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.HomeRowConfig
|
import com.github.damontecres.wholphin.data.model.HomeRowConfig
|
||||||
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
||||||
|
import com.github.damontecres.wholphin.ui.util.StringProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic state for loading something from the API
|
* Generic state for loading something from the API
|
||||||
|
|
@ -48,28 +49,28 @@ sealed interface RowLoadingState {
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface HomeRowLoadingState {
|
sealed interface HomeRowLoadingState {
|
||||||
val title: String
|
val title: StringProvider
|
||||||
|
|
||||||
val completed: Boolean
|
val completed: Boolean
|
||||||
get() = this is Success || this is Error
|
get() = this is Success || this is Error
|
||||||
|
|
||||||
data class Pending(
|
data class Pending(
|
||||||
override val title: String,
|
override val title: StringProvider,
|
||||||
) : HomeRowLoadingState
|
) : HomeRowLoadingState
|
||||||
|
|
||||||
data class Loading(
|
data class Loading(
|
||||||
override val title: String,
|
override val title: StringProvider,
|
||||||
) : HomeRowLoadingState
|
) : HomeRowLoadingState
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
override val title: String,
|
override val title: StringProvider,
|
||||||
val items: List<BaseItem?>,
|
val items: List<BaseItem?>,
|
||||||
val viewOptions: HomeRowViewOptions = HomeRowViewOptions(),
|
val viewOptions: HomeRowViewOptions = HomeRowViewOptions(),
|
||||||
val rowType: HomeRowConfig? = null,
|
val rowType: HomeRowConfig? = null,
|
||||||
) : HomeRowLoadingState
|
) : HomeRowLoadingState
|
||||||
|
|
||||||
data class Error(
|
data class Error(
|
||||||
override val title: String,
|
override val title: StringProvider,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val exception: Throwable? = null,
|
val exception: Throwable? = null,
|
||||||
) : HomeRowLoadingState {
|
) : HomeRowLoadingState {
|
||||||
|
|
|
||||||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
unqualifiedResLocale=en-US
|
||||||
|
|
@ -793,6 +793,7 @@
|
||||||
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
||||||
<string name="play_with">Play with</string>
|
<string name="play_with">Play with</string>
|
||||||
<string name="transcoding">Transcoding</string>
|
<string name="transcoding">Transcoding</string>
|
||||||
|
<string name="user_interface_language">User interface language</string>
|
||||||
<string name="display_toggles_title">Ratings display</string>
|
<string name="display_toggles_title">Ratings display</string>
|
||||||
<string name="display_toggles_summary">Customize which to display</string>
|
<string name="display_toggles_summary">Customize which to display</string>
|
||||||
<string name="profile_protection">Profile protection</string>
|
<string name="profile_protection">Profile protection</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue