diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f7df2fb5..42603452 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -184,6 +184,9 @@ configure { lint { disable.add("MissingTranslation") } + androidResources { + generateLocaleConfig = true + } } androidComponents { diff --git a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/33.json b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/33.json new file mode 100644 index 00000000..c8d73321 --- /dev/null +++ b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/33.json @@ -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')" + ] + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 51377cb8..64e7493c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -84,6 +84,15 @@ + + + + diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt index cfd8c41e..69f2e745 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt @@ -46,9 +46,8 @@ import java.util.UUID ItemTrackModification::class, SeerrServer::class, SeerrUser::class, - ], - version = 32, + version = 33, exportSchema = true, autoMigrations = [ AutoMigration(3, 4), @@ -64,6 +63,7 @@ import java.util.UUID AutoMigration(20, 30), AutoMigration(30, 31), AutoMigration(31, 32), + AutoMigration(32, 33), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt index c81afacc..ff568a37 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt @@ -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.components.ViewOptions import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.util.ResStringProvider import com.github.damontecres.wholphin.util.GetItemsRequestHandler import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.ExtraType @@ -41,8 +42,7 @@ sealed interface ExtrasItem { ) : ExtrasItem { override val destination: Destination = Destination.ItemGrid( - title = null, - titleRes = type.stringRes, + title = ResStringProvider(type.stringRes), request = GetItemsRequest( ids = items.map { it.id }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt index be966fc4..a357de4a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt @@ -64,6 +64,7 @@ data class JellyfinUser( @ColumnInfo(defaultValue = "false") val requireLogin: Boolean = false, val lastUsed: ZonedDateTime? = null, + val uiLanguage: String? = null, ) { val hasPin: Boolean get() = pin.isNotNullOrBlank() diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 228b31c2..6c92358c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -704,6 +704,13 @@ sealed interface AppPreference { summary = R.string.customize_home_summary, ) + val UserInterfaceLanguage = + AppClickablePreference( + title = R.string.user_interface_language, + getter = { }, + setter = { prefs, _ -> prefs }, + ) + val SendCrashReports = AppSwitchPreference( title = R.string.send_crash_reports, @@ -1110,6 +1117,7 @@ val basicPreferences = AppPreference.ProtectProfilePreference, AppPreference.CustomizeHome, AppPreference.UserPinnedNavDrawerItems, + AppPreference.UserInterfaceLanguage, ), ), PreferenceGroup( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index d460b9e4..c40a1057 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -1,6 +1,7 @@ package com.github.damontecres.wholphin.services import android.content.Context +import androidx.annotation.StringRes import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ServerRepository 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.toBaseItems 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.GetGenresRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -275,11 +281,11 @@ class HomeSettingsService libraries .mapIndexed { index, it -> val parentId = it.itemId - val title = getRecentlyAddedTitle(context, it) + val title = getRecentlyAddedTitle(it) if (it.collectionType == CollectionType.LIVETV) { HomeRowConfigDisplay( id = index, - title = context.getString(R.string.live_tv), + title = ResStringProvider(R.string.live_tv), config = HomeRowConfig.TvPrograms(), ) } else { @@ -295,7 +301,7 @@ class HomeSettingsService listOf( HomeRowConfigDisplay( id = includedIds.size + 1, - title = context.getString(R.string.combine_continue_next), + title = ResStringProvider(R.string.combine_continue_next), config = HomeRowConfig.ContinueWatchingCombined(), ), ) @@ -303,12 +309,12 @@ class HomeSettingsService listOf( HomeRowConfigDisplay( id = includedIds.size + 1, - title = context.getString(R.string.continue_watching), + title = ResStringProvider(R.string.continue_watching), config = HomeRowConfig.ContinueWatching(), ), HomeRowConfigDisplay( id = includedIds.size + 2, - title = context.getString(R.string.next_up), + title = ResStringProvider(R.string.next_up), config = HomeRowConfig.NextUp(), ), ) @@ -355,7 +361,7 @@ class HomeSettingsService HomeSectionType.ACTIVE_RECORDINGS -> { HomeRowConfigDisplay( id = id++, - title = context.getString(R.string.active_recordings), + title = ResStringProvider(R.string.active_recordings), config = HomeRowConfig.Recordings(), ) } @@ -363,7 +369,7 @@ class HomeSettingsService HomeSectionType.RESUME -> { HomeRowConfigDisplay( id = id++, - title = context.getString(R.string.continue_watching), + title = ResStringProvider(R.string.continue_watching), config = HomeRowConfig.ContinueWatching(), ) } @@ -371,7 +377,7 @@ class HomeSettingsService HomeSectionType.NEXT_UP -> { HomeRowConfigDisplay( id = id++, - title = context.getString(R.string.next_up), + title = ResStringProvider(R.string.next_up), config = HomeRowConfig.NextUp(), ) } @@ -380,7 +386,7 @@ class HomeSettingsService if (userDto.tvAccess) { HomeRowConfigDisplay( id = id++, - title = context.getString(R.string.live_tv), + title = ResStringProvider(R.string.live_tv), config = HomeRowConfig.TvPrograms(), ) } else { @@ -413,7 +419,7 @@ class HomeSettingsService HomeRowConfigDisplay( id = id++, title = - context.getString( + ResArgStringProvider( R.string.recently_added_in, it.name ?: "", ), @@ -441,7 +447,7 @@ class HomeSettingsService ): HomeRowConfigDisplay = when (config) { is HomeRowConfig.ByParent -> { - val name = getItemName(config.parentId) ?: "" + val name = getItemName(null, config.parentId) HomeRowConfigDisplay( id, name, @@ -452,7 +458,7 @@ class HomeSettingsService is HomeRowConfig.ContinueWatching -> { HomeRowConfigDisplay( id, - context.getString(R.string.continue_watching), + ResStringProvider(R.string.continue_watching), config, ) } @@ -460,64 +466,64 @@ class HomeSettingsService is HomeRowConfig.ContinueWatchingCombined -> { HomeRowConfigDisplay( id, - context.getString(R.string.combine_continue_next), + ResStringProvider(R.string.combine_continue_next), config, ) } is HomeRowConfig.Genres -> { - val name = getItemName(config.parentId) ?: "" + val title = getItemName(R.string.genres_in, config.parentId) HomeRowConfigDisplay( id, - context.getString(R.string.genres_in, name), + title, config, ) } is HomeRowConfig.Studios -> { - val name = getItemName(config.parentId) ?: "" + val title = getItemName(R.string.studios_in, config.parentId) HomeRowConfigDisplay( id, - context.getString(R.string.studios_in, name), + title, config, ) } is HomeRowConfig.GetItems -> { - HomeRowConfigDisplay(id, config.name, config) + HomeRowConfigDisplay(id, StringStringProvider(config.name), config) } is HomeRowConfig.NextUp -> { HomeRowConfigDisplay( id, - context.getString(R.string.next_up), + ResStringProvider(R.string.next_up), config, ) } is HomeRowConfig.RecentlyAdded -> { - val name = getItemName(config.parentId) ?: "" + val title = getItemName(R.string.recently_added_in, config.parentId) HomeRowConfigDisplay( id, - context.getString(R.string.recently_added_in, name), + title, config, ) } is HomeRowConfig.RecentlyReleased -> { - val name = getItemName(config.parentId) ?: "" + val title = getItemName(R.string.recently_released_in, config.parentId) HomeRowConfigDisplay( id, - context.getString(R.string.recently_released_in, name), + title, config, ) } is HomeRowConfig.Favorite -> { val name = - context.getString( + ResProviderStringProvider( R.string.favorite_items, - context.getString(favoriteOptions[config.kind]!!), + ResStringProvider(favoriteOptions[config.kind]!!), ) HomeRowConfigDisplay(id, name, config) } @@ -525,7 +531,7 @@ class HomeSettingsService is HomeRowConfig.Recordings -> { HomeRowConfigDisplay( id = id, - title = context.getString(R.string.active_recordings), + title = ResStringProvider(R.string.active_recordings), config, ) } @@ -533,7 +539,7 @@ class HomeSettingsService is HomeRowConfig.TvPrograms -> { HomeRowConfigDisplay( id = id, - title = context.getString(R.string.live_tv), + title = ResStringProvider(R.string.live_tv), config, ) } @@ -541,29 +547,40 @@ class HomeSettingsService is HomeRowConfig.TvChannels -> { HomeRowConfigDisplay( id = id, - title = context.getString(R.string.channels), + title = ResStringProvider(R.string.channels), config, ) } is HomeRowConfig.Suggestions -> { - val name = getItemName(config.parentId) ?: "" + val title = getItemName(R.string.suggestions_for, config.parentId) HomeRowConfigDisplay( id = id, - title = context.getString(R.string.suggestions_for, name), + title = title, config, ) } } - private suspend fun getItemName(itemId: UUID): String? = + private suspend fun getItemName( + @StringRes stringRes: Int?, + itemId: UUID, + default: StringProvider = StringStringProvider(""), + ): StringProvider = try { api.userLibraryApi .getItem(itemId = itemId) .content.name + ?.let { + if (stringRes == null) { + StringStringProvider(it) + } else { + ResArgStringProvider(stringRes, it) + } + } ?: default } catch (ex: Exception) { 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( - title = context.getString(R.string.continue_watching), + title = ResStringProvider(R.string.continue_watching), items = resume, viewOptions = row.viewOptions, rowType = row, @@ -609,7 +626,7 @@ class HomeSettingsService ) Success( - title = context.getString(R.string.next_up), + title = ResStringProvider(R.string.next_up), items = nextUp, viewOptions = row.viewOptions, rowType = row, @@ -635,7 +652,7 @@ class HomeSettingsService ) Success( - title = context.getString(R.string.continue_watching), + title = ResStringProvider(R.string.continue_watching), items = latestNextUpService .buildCombined( @@ -676,8 +693,8 @@ class HomeSettingsService .firstOrNull { it.itemId == row.parentId } val title = - library?.name?.let { context.getString(R.string.genres_in, it) } - ?: context.getString(R.string.genres) + library?.name?.let { ResArgStringProvider(R.string.genres_in, it) } + ?: ResStringProvider(R.string.genres) val genres = items.map { BaseItem( @@ -723,8 +740,8 @@ class HomeSettingsService libraries .firstOrNull { it.itemId == row.parentId } val title = - library?.name?.let { context.getString(R.string.studios_in, it) } - ?: context.getString(R.string.studios) + library?.name?.let { ResArgStringProvider(R.string.studios_in, it) } + ?: ResStringProvider(R.string.studios) val studios = items.map { val imageUrl = @@ -762,7 +779,7 @@ class HomeSettingsService val library = libraries .firstOrNull { it.itemId == row.parentId } - val title = getRecentlyAddedTitle(context, library) + val title = getRecentlyAddedTitle(library) val request = GetLatestMediaRequest( fields = SlimItemFields, @@ -795,8 +812,8 @@ class HomeSettingsService ?.name val title = name?.let { - context.getString(R.string.recently_released_in, it) - } ?: context.getString(R.string.recently_released) + ResArgStringProvider(R.string.recently_released_in, it) + } ?: ResStringProvider(R.string.recently_released) val request = GetItemsRequest( parentId = row.parentId, @@ -853,7 +870,8 @@ class HomeSettingsService fields = DefaultItemFields, ) - val name = getItemName(row.parentId) + val title = + getItemName(null, row.parentId, ResStringProvider(R.string.collection)) if (usePaging) { ApiRequestPager( api, @@ -869,7 +887,7 @@ class HomeSettingsService .map { BaseItem(it, row.viewOptions.useSeries) } }.let { Success( - name ?: context.getString(R.string.collection), + title, it, row.viewOptions, rowType = row, @@ -906,7 +924,7 @@ class HomeSettingsService .map { BaseItem(it, row.viewOptions.useSeries) } }.let { Success( - row.name, + StringStringProvider(row.name), it, row.viewOptions, rowType = row, @@ -916,9 +934,9 @@ class HomeSettingsService is HomeRowConfig.Favorite -> { val title = - context.getString( + ResProviderStringProvider( R.string.favorite_items, - context.getString(favoriteOptions[row.kind]!!), + ResStringProvider(favoriteOptions[row.kind]!!), ) if (row.kind == BaseItemKind.PERSON) { val request = @@ -1001,7 +1019,7 @@ class HomeSettingsService .map { BaseItem(it, row.viewOptions.useSeries) } }.let { Success( - context.getString(R.string.active_recordings), + ResStringProvider(R.string.active_recordings), it, row.viewOptions, rowType = row, @@ -1027,7 +1045,7 @@ class HomeSettingsService .map { BaseItem(it, row.viewOptions.useSeries) } .let { Success( - context.getString(R.string.live_tv), + ResStringProvider(R.string.live_tv), it, row.viewOptions, rowType = row, @@ -1057,7 +1075,7 @@ class HomeSettingsService .toBaseItems(api, row.viewOptions.useSeries) }.let { Success( - context.getString(R.string.channels), + ResStringProvider(R.string.channels), it, row.viewOptions, rowType = row, @@ -1070,7 +1088,7 @@ class HomeSettingsService api.userLibraryApi .getItem(itemId = row.parentId) .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 suggestions = itemKind?.let { @@ -1111,7 +1129,7 @@ class HomeSettingsService */ data class HomeRowConfigDisplay( val id: Int, - val title: String, + val title: StringProvider, val config: HomeRowConfig, ) @@ -1154,13 +1172,10 @@ class UnsupportedHomeSettingsVersionException( val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION, ) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion") -fun getRecentlyAddedTitle( - context: Context, - library: Library?, -): String = +fun getRecentlyAddedTitle(library: Library?): StringProvider = if (library?.isRecordingFolder == true) { - context.getString(R.string.recently_recorded) + ResStringProvider(R.string.recently_recorded) } else { - library?.name?.let { context.getString(R.string.recently_added_in, it) } - ?: context.getString(R.string.recently_added) + library?.name?.let { ResArgStringProvider(R.string.recently_added_in, it) } + ?: ResStringProvider(R.string.recently_added) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt index 7096b833..e2086c29 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -1,10 +1,5 @@ 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.model.AuthJellyfinPostRequest 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.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.data.model.SeerrPermission import com.github.damontecres.wholphin.data.model.SeerrServer import com.github.damontecres.wholphin.data.model.SeerrUser import com.github.damontecres.wholphin.data.model.hasPermission 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.util.LoadingState -import dagger.hilt.android.qualifiers.ActivityContext -import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -import kotlinx.coroutines.supervisorScope import okhttp3.OkHttpClient import org.jellyfin.sdk.model.api.ImageType -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.seconds @@ -140,7 +129,7 @@ class SeerrServerRepository serverRepository.currentUser.value?.let { jellyfinUser -> // TODO Need to update server early so that cookies are saved seerrApi.update(server.url, null) - val userConfig = login(seerrApi.api, authMethod, username, password) + val userConfig = seerrLogin(seerrApi.api, authMethod, username, password) val user = SeerrUser( @@ -174,7 +163,7 @@ class SeerrServerRepository .readTimeout(6.seconds) .build(), ) - login(api, authMethod, username, passwordOrApiKey) + seerrLogin(api, authMethod, username, passwordOrApiKey) return LoadingState.Success } @@ -227,7 +216,7 @@ data class CurrentSeerr( config.hasPermission(SeerrPermission.REQUEST_4K_TV) } -private suspend fun login( +suspend fun seerrLogin( client: SeerrApiClient, authMethod: SeerrAuthMethod, 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( imageType: ImageType, path: String?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UserSwitchListener.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UserSwitchListener.kt new file mode 100644 index 00000000..352b8439 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UserSwitchListener.kt @@ -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) + } + } + } + } + } + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt index e2111637..c41c27f0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt @@ -1,6 +1,6 @@ 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.automirrored.filled.ArrowForward 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.setValue 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.unit.dp import com.github.damontecres.wholphin.R @@ -184,15 +184,15 @@ fun ContextMenu( ) { val item = contextMenu.item val chosenStreams = contextMenu.chosenStreams - val context = LocalContext.current + val resources = LocalResources.current var chooseVersion by remember { mutableStateOf(null) } var showDeleteDialog by remember { mutableStateOf(false) } var showPlayWithDialog by remember { mutableStateOf(false) } val dialogItems = - remember(context, item, chosenStreams) { + remember(resources, item, chosenStreams) { buildContextMenuItems( - context = context, + resources = resources, item = item, watched = item.played, favorite = item.favorite, @@ -208,7 +208,7 @@ fun ContextMenu( onChooseVersion = { chooseVersion = chooseVersionParams( - context, + resources, item.data.mediaSources.orEmpty(), chosenStreams?.source?.id?.toUUIDOrNull(), ) { idx -> @@ -225,7 +225,7 @@ fun ContextMenu( )?.let { source -> chooseVersion = chooseStream( - context = context, + resources = resources, streams = source.mediaStreams.orEmpty(), type = type, currentIndex = @@ -290,7 +290,7 @@ fun ContextMenu( if (showPlayWithDialog) { val dialogItems = remember { - buildPlayWith(context) { transcode, backend -> + buildPlayWith(resources) { transcode, backend -> onDismissRequest.invoke() actions.navigateTo( Destination.Playback( @@ -315,7 +315,7 @@ fun ContextMenu( } private fun buildContextMenuItems( - context: Context, + resources: Resources, item: BaseItem, seriesId: UUID?, sourceId: UUID?, @@ -339,7 +339,7 @@ private fun buildContextMenuItems( if (showGoTo) { add( DialogItem( - context.getString(R.string.go_to), + resources.getString(R.string.go_to), Icons.Default.ArrowForward, dismissOnClick = true, ) { @@ -351,7 +351,7 @@ private fun buildContextMenuItems( if (item.playbackPosition > Duration.ZERO) { add( DialogItem( - context.getString(R.string.resume), + resources.getString(R.string.resume), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, @@ -366,7 +366,7 @@ private fun buildContextMenuItems( ) add( DialogItem( - context.getString(R.string.restart), + resources.getString(R.string.restart), Icons.Default.Refresh, dismissOnClick = true, ) { @@ -381,7 +381,7 @@ private fun buildContextMenuItems( } else { add( DialogItem( - context.getString(R.string.play), + resources.getString(R.string.play), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, @@ -407,9 +407,9 @@ private fun buildContextMenuItems( if (audioCount > 1) { add( DialogItem( - context.getString( + resources.getString( R.string.choose_stream, - context.getString(R.string.audio), + resources.getString(R.string.audio), ), R.string.fa_volume_low, dismissOnClick = false, @@ -421,9 +421,9 @@ private fun buildContextMenuItems( if (subtitleCount > 0) { add( DialogItem( - context.getString( + resources.getString( R.string.choose_stream, - context.getString(R.string.subtitles), + resources.getString(R.string.subtitles), ), R.string.fa_closed_captioning, dismissOnClick = false, @@ -436,9 +436,9 @@ private fun buildContextMenuItems( if (sources.size > 1) { add( DialogItem( - context.getString( + resources.getString( R.string.choose_stream, - context.getString(R.string.version), + resources.getString(R.string.version), ), R.string.fa_file_video, dismissOnClick = false, @@ -452,7 +452,7 @@ private fun buildContextMenuItems( if (item.type == BaseItemKind.MUSIC_ALBUM) { add( DialogItem( - context.getString(R.string.add_to_queue), + resources.getString(R.string.add_to_queue), Icons.Default.Add, dismissOnClick = true, ) { @@ -472,7 +472,7 @@ private fun buildContextMenuItems( if (canDelete) { add( DialogItem( - context.getString(R.string.delete), + resources.getString(R.string.delete), Icons.Default.Delete, iconColor = Color.Red.copy(alpha = .8f), dismissOnClick = false, @@ -525,7 +525,7 @@ private fun buildContextMenuItems( item.data.albumId?.let { albumId -> add( DialogItem( - context.getString(R.string.go_to_album), + resources.getString(R.string.go_to_album), R.string.fa_compact_disc, dismissOnClick = true, ) { @@ -542,7 +542,7 @@ private fun buildContextMenuItems( item.data.artistItems?.firstOrNull()?.id?.let { artistId -> add( DialogItem( - context.getString(R.string.go_to_artist), + resources.getString(R.string.go_to_artist), R.string.fa_user, dismissOnClick = true, ) { @@ -559,7 +559,7 @@ private fun buildContextMenuItems( seriesId?.let { add( DialogItem( - context.getString(R.string.go_to_series), + resources.getString(R.string.go_to_series), Icons.AutoMirrored.Filled.ArrowForward, dismissOnClick = true, ) { @@ -576,7 +576,7 @@ private fun buildContextMenuItems( if (item.data.mediaSources?.isNotEmpty() == true) { add( DialogItem( - context.getString(R.string.media_information), + resources.getString(R.string.media_information), Icons.Default.Info, dismissOnClick = true, ) { @@ -587,7 +587,7 @@ private fun buildContextMenuItems( if (showStreamChoices && canClearChosenStreams) { add( DialogItem( - context.getString(R.string.clear_track_choices), + resources.getString(R.string.clear_track_choices), Icons.Default.Delete, dismissOnClick = true, ) { @@ -598,7 +598,7 @@ private fun buildContextMenuItems( if (item.type in supportedPlayableTypes) { add( DialogItem( - context.getString(R.string.play_with), + resources.getString(R.string.play_with), Icons.Default.PlayArrow, dismissOnClick = false, onClick = onClickPlayWith, @@ -619,13 +619,13 @@ private fun buildContextMenuItems( } private fun buildPlayWith( - context: Context, + resources: Resources, onClick: (Boolean, PlayerBackend?) -> Unit, ) = buildList { val entries = PlayerBackend.entries .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 } entries.forEach { (backend, title) -> add( @@ -639,7 +639,7 @@ private fun buildPlayWith( } add( DialogItem( - context.getString(R.string.transcoding), + resources.getString(R.string.transcoding), dismissOnClick = true, ) { onClick.invoke(true, null) @@ -691,11 +691,11 @@ fun ContextMenu( item: ContextMenu.ForMusic, actions: MusicContextActions, ) { - val context = LocalContext.current + val resources = LocalResources.current var showDeleteDialog by remember { mutableStateOf(false) } val dialogItems = - remember(context, item, actions) { - buildContextForMusic(context, item, actions, onClickDelete = { + remember(resources, item, actions) { + buildContextForMusic(resources, item, actions, onClickDelete = { showDeleteDialog = true }) } @@ -720,7 +720,7 @@ fun ContextMenu( } fun buildContextForMusic( - context: Context, + resources: Resources, music: ContextMenu.ForMusic, actions: MusicContextActions, onClickDelete: () -> Unit, @@ -730,7 +730,7 @@ fun buildContextForMusic( val index = music.index add( DialogItem( - context.getString(R.string.play), + resources.getString(R.string.play), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, @@ -740,7 +740,7 @@ fun buildContextForMusic( ) add( DialogItem( - context.getString(R.string.play_next), + resources.getString(R.string.play_next), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, @@ -751,7 +751,7 @@ fun buildContextForMusic( if (music.canRemoveFromQueue) { add( DialogItem( - context.getString(R.string.remove_from_queue), + resources.getString(R.string.remove_from_queue), Icons.Default.Delete, dismissOnClick = true, ) { @@ -761,7 +761,7 @@ fun buildContextForMusic( } else { add( DialogItem( - context.getString(R.string.add_to_queue), + resources.getString(R.string.add_to_queue), Icons.Default.Add, dismissOnClick = true, ) { @@ -781,7 +781,7 @@ fun buildContextForMusic( if (music.canDelete) { add( DialogItem( - context.getString(R.string.delete), + resources.getString(R.string.delete), Icons.Default.Delete, iconColor = Color.Red.copy(alpha = .8f), dismissOnClick = false, @@ -803,7 +803,7 @@ fun buildContextForMusic( if (item.type == BaseItemKind.AUDIO && item.data.albumId != null) { add( DialogItem( - context.getString(R.string.go_to_album), + resources.getString(R.string.go_to_album), R.string.fa_compact_disc, dismissOnClick = true, ) { @@ -814,7 +814,7 @@ fun buildContextForMusic( if ((item.type == BaseItemKind.AUDIO || item.type == BaseItemKind.MUSIC_ALBUM) && item.data.artistItems?.isNotEmpty() == true) { add( DialogItem( - context.getString(R.string.go_to_artist), + resources.getString(R.string.go_to_artist), R.string.fa_user, dismissOnClick = true, ) { @@ -834,11 +834,11 @@ fun ContextMenu( item: ContextMenu.ForQueue, actions: QueueContextActions, ) { - val context = LocalContext.current + val resources = LocalResources.current val dialogItems = - remember(context, item, actions) { + remember(resources, item, actions) { buildContextForMusicQueue( - context, + resources, item.item, item.index, actions, @@ -855,7 +855,7 @@ fun ContextMenu( } fun buildContextForMusicQueue( - context: Context, + resources: Resources, item: AudioItem, index: Int, actions: QueueContextActions, @@ -863,7 +863,7 @@ fun buildContextForMusicQueue( buildList { add( DialogItem( - context.getString(R.string.play), + resources.getString(R.string.play), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, @@ -873,7 +873,7 @@ fun buildContextForMusicQueue( ) add( DialogItem( - context.getString(R.string.play_next), + resources.getString(R.string.play_next), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, @@ -883,7 +883,7 @@ fun buildContextForMusicQueue( ) add( DialogItem( - context.getString(R.string.remove_from_queue), + resources.getString(R.string.remove_from_queue), Icons.Default.Delete, dismissOnClick = true, ) { @@ -893,7 +893,7 @@ fun buildContextForMusicQueue( if (item.albumId != null) { add( DialogItem( - context.getString(R.string.go_to_album), + resources.getString(R.string.go_to_album), Icons.Default.ArrowForward, dismissOnClick = true, ) { @@ -904,7 +904,7 @@ fun buildContextForMusicQueue( if (item.artistId != null) { add( DialogItem( - context.getString(R.string.go_to_artist), + resources.getString(R.string.go_to_artist), Icons.Default.ArrowForward, dismissOnClick = true, ) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index 58bcde20..6bd3dfaa 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -1,6 +1,6 @@ package com.github.damontecres.wholphin.ui.components -import android.content.Context +import android.content.res.Resources import android.view.KeyEvent import androidx.annotation.StringRes import androidx.compose.foundation.background @@ -608,14 +608,14 @@ fun ConfirmDeleteDialog( } fun chooseVersionParams( - context: Context, + resources: Resources, sources: List, chosenSourceId: UUID?, onClick: (Int) -> Unit, ): DialogParams = DialogParams( 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 = sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source -> val uuid = source.id?.toUUIDOrNull() @@ -649,7 +649,7 @@ fun resourceFor(type: MediaStreamType): Int = } fun chooseStream( - context: Context, + resources: Resources, streams: List, currentIndex: Int?, type: MediaStreamType, @@ -658,7 +658,7 @@ fun chooseStream( ): DialogParams = DialogParams( 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 = buildList { if (type == MediaStreamType.SUBTITLE) { @@ -700,7 +700,7 @@ fun chooseStream( it } }.mapIndexed { index, stream -> - val simpleStream = SimpleMediaStream.from(context, stream, true) + val simpleStream = SimpleMediaStream.from(resources, stream, true) DialogItem( selected = currentIndex == stream.index, leadingContent = { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt index 1aba204f..12c7c56e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -112,7 +111,7 @@ fun ItemGrid( } Column(modifier = modifier) { Text( - text = destination.title ?: destination.titleRes?.let { stringResource(it) } ?: "", + text = destination.title.getString(), style = MaterialTheme.typography.displayMedium, color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt index 080e6bb7..2c30f337 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder @@ -132,14 +133,14 @@ fun TimeRemaining( modifier: Modifier = Modifier, textStyle: TextStyle = MaterialTheme.typography.titleSmall, ) { - val context = LocalContext.current + val resources = LocalResources.current val now by LocalClock.current.now val remainingStr = - remember(remaining, now) { + remember(remaining, now, resources) { val endTimeStr = getTimeFormatter().format(now.plusSeconds(remaining.inWholeSeconds)) buildAnnotatedString { dot() - append(context.getString(R.string.ends_at, endTimeStr)) + append(resources.getString(R.string.ends_at, endTimeStr)) } } Text( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index 72c2888f..85328920 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -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.rememberPosition 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.GetItemsRequestHandler import com.github.damontecres.wholphin.util.HomeRowLoadingState @@ -111,13 +112,13 @@ class RecommendedViewModel it.copy( loading = LoadingState.Loading, rows = - recommendedRows.map { HomeRowLoadingState.Loading(context.getString(it.title)) } + - listOf(HomeRowLoadingState.Loading(context.getString(R.string.suggestions))), + recommendedRows.map { HomeRowLoadingState.Loading(ResStringProvider(it.title)) } + + listOf(HomeRowLoadingState.Loading(ResStringProvider(R.string.suggestions))), ) } val jobs = recommendedRows.mapIndexed { index, row -> - val title = context.getString(row.title) + val title = ResStringProvider(row.title) viewModelScope.launchIO { val result = try { @@ -173,7 +174,7 @@ class RecommendedViewModel private fun fetchSuggestions() { viewModelScope.launch(Dispatchers.IO) { - val title = context.getString(R.string.suggestions) + val title = ResStringProvider(R.string.suggestions) try { suggestionService .getSuggestionsFlow(parentId, suggestionsType) @@ -298,7 +299,6 @@ class RecommendedViewModel navigationManager.navigateTo( Destination.ItemGrid( title = row.title, - titleRes = recommendedRow.title, request = recommendedRow.request, requestHandler = recommendedRow.handler, initialPosition = row.items.size, @@ -313,7 +313,6 @@ class RecommendedViewModel navigationManager.navigateTo( Destination.ItemGrid( title = row.title, - titleRes = R.string.suggestions, request = GetItemsRequest( ids = row.items.mapNotNull { it?.id }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt index ddfb7f0f..083dded6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/VideoStreamDetails.kt @@ -17,7 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -69,7 +69,7 @@ fun VideoStreamDetails( numberOfVersions: Int = 0, modifier: Modifier = Modifier, ) { - val context = LocalContext.current + val resources = LocalResources.current Row( horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier, @@ -90,7 +90,13 @@ fun VideoStreamDetails( } else { null } - val range = formatVideoRange(context, it.videoRange, it.videoRangeType, it.videoDoViTitle) + val range = + formatVideoRange( + resources, + it.videoRange, + it.videoRangeType, + it.videoDoViTitle, + ) resName.concatWithSpace(range) } } @@ -108,11 +114,11 @@ fun VideoStreamDetails( val audio = remember(audioCount, audioStream) { if (audioCount == 0 || audioStream == null) { - context.getString(R.string.none) + resources.getString(R.string.none) } else { listOfNotNull( languageName(audioStream.language), - formatAudioCodec(context, audioStream.codec, audioStream.profile), + formatAudioCodec(resources, audioStream.codec, audioStream.profile), audioStream.channelLayout, ).joinToString(" ") } @@ -133,7 +139,7 @@ fun VideoStreamDetails( remember(subtitleCount, subtitleStream) { if (subtitleCount > 0 && subtitleStream == null) { disabled = true - context.getString(R.string.none) + resources.getString(R.string.none) } else if (subtitleCount == 0 || subtitleStream == null) { null } else { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt index 67e4cc3e..f0591868 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt @@ -1,6 +1,6 @@ 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.Column import androidx.compose.foundation.layout.Row @@ -13,7 +13,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.remember 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.unit.dp import androidx.tv.material3.MaterialTheme @@ -66,7 +66,7 @@ fun ItemDetailsDialog( showFilePath: Boolean, onDismissRequest: () -> Unit, ) { - val context = LocalContext.current + val resources = LocalResources.current // Extract stringResource calls outside of ScrollableDialog's non-composable lambda val pathLabel = stringResource(R.string.path) val fileSizeLabel = stringResource(R.string.file_size) @@ -164,8 +164,8 @@ fun ItemDetailsDialog( itemsIndexed(videoStreams) { index, stream -> MediaInfoSection( title = titleIndex(videoLabel, index, videoStreams.size), - items = remember { buildVideoStreamInfo(context, stream) }, - additional = remember { buildVideoStreamInfoAdditional(context, stream) }, + items = remember { buildVideoStreamInfo(resources, stream) }, + additional = remember { buildVideoStreamInfoAdditional(resources, stream) }, ) } @@ -184,7 +184,7 @@ fun ItemDetailsDialog( groupIndex * 3 + index, audioStreams.size, ), - items = buildAudioStreamInfo(context, stream), + items = buildAudioStreamInfo(resources, stream), modifier = Modifier.weight(1f), ) } @@ -210,7 +210,7 @@ fun ItemDetailsDialog( groupIndex * 3 + index, subtitleStreams.size, ), - items = buildSubtitleStreamInfo(context, stream, showFilePath), + items = buildSubtitleStreamInfo(resources, stream, showFilePath), modifier = Modifier.weight(1f), ) } @@ -301,25 +301,25 @@ fun titleIndex( } private fun buildVideoStreamInfo( - context: Context, + resources: Resources, stream: MediaStream, ): List> = buildList { - val yesStr = context.getString(R.string.yes) - val noStr = context.getString(R.string.no) + val yesStr = resources.getString(R.string.yes) + val noStr = resources.getString(R.string.no) - val titleLabel = context.getString(R.string.title) - val codecLabel = context.getString(R.string.codec) - val resolutionLabel = context.getString(R.string.resolution) - val aspectRatioLabel = context.getString(R.string.aspect_ratio) - val framerateLabel = context.getString(R.string.framerate) - val bitrateLabel = context.getString(R.string.bitrate) - val profileLabel = context.getString(R.string.profile) - val levelLabel = context.getString(R.string.level) - val interlacedLabel = context.getString(R.string.interlaced) - val videoRangeLabel = context.getString(R.string.video_range) - val sdrStr = context.getString(R.string.sdr) - val hdrStr = context.getString(R.string.hdr) + val titleLabel = resources.getString(R.string.title) + val codecLabel = resources.getString(R.string.codec) + val resolutionLabel = resources.getString(R.string.resolution) + val aspectRatioLabel = resources.getString(R.string.aspect_ratio) + val framerateLabel = resources.getString(R.string.framerate) + val bitrateLabel = resources.getString(R.string.bitrate) + val profileLabel = resources.getString(R.string.profile) + val levelLabel = resources.getString(R.string.level) + val interlacedLabel = resources.getString(R.string.interlaced) + val videoRangeLabel = resources.getString(R.string.video_range) + val sdrStr = resources.getString(R.string.sdr) + val hdrStr = resources.getString(R.string.hdr) stream.title?.let { add(titleLabel to it) } stream.codec?.let { add(codecLabel to it.uppercase()) } @@ -350,31 +350,31 @@ private fun buildVideoStreamInfo( } private fun buildVideoStreamInfoAdditional( - context: Context, + resources: Resources, stream: MediaStream, ): List> = buildList { - val yesStr = context.getString(R.string.yes) - val noStr = context.getString(R.string.no) + val yesStr = resources.getString(R.string.yes) + val noStr = resources.getString(R.string.no) - val avcLabel = context.getString(R.string.avc) - val anamorphicLabel = context.getString(R.string.anamorphic) - val bitDepthLabel = context.getString(R.string.bit_depth) + val avcLabel = resources.getString(R.string.avc) + val anamorphicLabel = resources.getString(R.string.anamorphic) + val bitDepthLabel = resources.getString(R.string.bit_depth) - val videoRangeTypeLabel = context.getString(R.string.video_range_type) - val colorSpaceLabel = context.getString(R.string.color_space) - val colorTransferLabel = context.getString(R.string.color_transfer) - val colorPrimariesLabel = context.getString(R.string.color_primaries) - val pixelFormatLabel = context.getString(R.string.pixel_format) - val refFramesLabel = context.getString(R.string.ref_frames) - val nalLabel = context.getString(R.string.nal) - val dolbyVisionLabel = context.getString(R.string.dolby_vision) + val videoRangeTypeLabel = resources.getString(R.string.video_range_type) + val colorSpaceLabel = resources.getString(R.string.color_space) + val colorTransferLabel = resources.getString(R.string.color_transfer) + val colorPrimariesLabel = resources.getString(R.string.color_primaries) + val pixelFormatLabel = resources.getString(R.string.pixel_format) + val refFramesLabel = resources.getString(R.string.ref_frames) + val nalLabel = resources.getString(R.string.nal) + val dolbyVisionLabel = resources.getString(R.string.dolby_vision) - val sdrStr = context.getString(R.string.sdr) - val hdr10Str = context.getString(R.string.hdr10) - val hdr10PlusStr = context.getString(R.string.hdr10_plus) - val hlgStr = context.getString(R.string.hlg) - val bitUnit = context.getString(R.string.bit_unit) + val sdrStr = resources.getString(R.string.sdr) + val hdr10Str = resources.getString(R.string.hdr10) + val hdr10PlusStr = resources.getString(R.string.hdr10_plus) + val hlgStr = resources.getString(R.string.hlg) + val bitUnit = resources.getString(R.string.bit_unit) stream.isAvc?.let { add(avcLabel 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_HLG, VideoRangeType.DOVI_WITH_SDR, - -> context.getString(R.string.dolby_vision) + -> resources.getString(R.string.dolby_vision) VideoRangeType.UNKNOWN -> null @@ -412,27 +412,27 @@ private fun buildVideoStreamInfoAdditional( } private fun buildAudioStreamInfo( - context: Context, + resources: android.content.res.Resources, stream: MediaStream, ): List> = buildList { - val titleLabel = context.getString(R.string.title) - val languageLabel = context.getString(R.string.language) - val codecLabel = context.getString(R.string.codec) - val layoutLabel = context.getString(R.string.layout) - val channelsLabel = context.getString(R.string.channels) - val bitrateLabel = context.getString(R.string.bitrate) - val sampleRateLabel = context.getString(R.string.sample_rate) - val defaultLabel = context.getString(R.string.default_track) - val profileLabel = context.getString(R.string.profile) - val yesStr = context.getString(R.string.yes) - val noStr = context.getString(R.string.no) - val sampleRateUnit = context.getString(R.string.sample_rate_unit) + val titleLabel = resources.getString(R.string.title) + val languageLabel = resources.getString(R.string.language) + val codecLabel = resources.getString(R.string.codec) + val layoutLabel = resources.getString(R.string.layout) + val channelsLabel = resources.getString(R.string.channels) + val bitrateLabel = resources.getString(R.string.bitrate) + val sampleRateLabel = resources.getString(R.string.sample_rate) + val defaultLabel = resources.getString(R.string.default_track) + val profileLabel = resources.getString(R.string.profile) + val yesStr = resources.getString(R.string.yes) + val noStr = resources.getString(R.string.no) + val sampleRateUnit = resources.getString(R.string.sample_rate_unit) stream.title?.let { add(titleLabel to it) } stream.language?.let { add(languageLabel to languageName(it)) } stream.codec?.let { - val formattedCodec = formatAudioCodec(context, it, stream.profile) + " ($it)" + val formattedCodec = formatAudioCodec(resources, it, stream.profile) + " ($it)" add(codecLabel to formattedCodec) } stream.channelLayout?.let { add(layoutLabel to it) } @@ -444,20 +444,20 @@ private fun buildAudioStreamInfo( } private fun buildSubtitleStreamInfo( - context: Context, + resources: Resources, stream: MediaStream, showPath: Boolean, ): List> = buildList { - val titleLabel = context.getString(R.string.title) - val languageLabel = context.getString(R.string.language) - val codecLabel = context.getString(R.string.codec) - val defaultLabel = context.getString(R.string.default_track) - val forcedLabel = context.getString(R.string.forced_track) - val externalLabel = context.getString(R.string.external_track) - val yesStr = context.getString(R.string.yes) - val noStr = context.getString(R.string.no) - val pathLabel = context.getString(R.string.path) + val titleLabel = resources.getString(R.string.title) + val languageLabel = resources.getString(R.string.language) + val codecLabel = resources.getString(R.string.codec) + val defaultLabel = resources.getString(R.string.default_track) + val forcedLabel = resources.getString(R.string.forced_track) + val externalLabel = resources.getString(R.string.external_track) + val yesStr = resources.getString(R.string.yes) + val noStr = resources.getString(R.string.no) + val pathLabel = resources.getString(R.string.path) stream.title?.let { add(titleLabel to it) } stream.language?.let { add(languageLabel to languageName(it)) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt index 20172832..73ad4a2e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt @@ -1,6 +1,6 @@ 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.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.Add @@ -32,7 +32,7 @@ data class MoreDialogActions( ) fun buildMoreDialogItemsForHome( - context: Context, + resources: Resources, item: BaseItem, seriesId: UUID?, playbackPosition: Duration, @@ -47,7 +47,7 @@ fun buildMoreDialogItemsForHome( val itemId = item.id add( DialogItem( - context.getString(R.string.go_to), + resources.getString(R.string.go_to), Icons.Default.ArrowForward, ) { actions.onClickGoTo(item) @@ -57,7 +57,7 @@ fun buildMoreDialogItemsForHome( if (playbackPosition >= 1.seconds) { add( DialogItem( - context.getString(R.string.resume), + resources.getString(R.string.resume), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), ) { @@ -71,7 +71,7 @@ fun buildMoreDialogItemsForHome( ) add( DialogItem( - context.getString(R.string.restart), + resources.getString(R.string.restart), Icons.Default.Refresh, // iconColor = Color.Green.copy(alpha = .8f), ) { @@ -86,7 +86,7 @@ fun buildMoreDialogItemsForHome( } else { add( DialogItem( - context.getString(R.string.play), + resources.getString(R.string.play), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), ) { @@ -103,7 +103,7 @@ fun buildMoreDialogItemsForHome( if (item.type == BaseItemKind.MUSIC_ALBUM) { add( DialogItem( - context.getString(R.string.add_to_queue), + resources.getString(R.string.add_to_queue), Icons.Default.Add, ) { actions.onClickAddToQueue(item) @@ -121,7 +121,7 @@ fun buildMoreDialogItemsForHome( if (canDelete) { add( DialogItem( - context.getString(R.string.delete), + resources.getString(R.string.delete), Icons.Default.Delete, iconColor = Color.Red.copy(alpha = .8f), ) { @@ -169,7 +169,7 @@ fun buildMoreDialogItemsForHome( seriesId?.let { add( DialogItem( - context.getString(R.string.go_to_series), + resources.getString(R.string.go_to_series), Icons.AutoMirrored.Filled.ArrowForward, ) { actions.navigateTo( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt index a83858cc..fc088cf7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/HomeRowGrid.kt @@ -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.rememberInt 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.ExceptionHandler import com.github.damontecres.wholphin.util.HomeRowLoadingState @@ -93,13 +95,13 @@ class HomeRowGridViewModel private val musicService: MusicService, val streamChoiceService: StreamChoiceService, val mediaReportService: MediaReportService, - @Assisted private val title: String, + @Assisted private val title: StringProvider, @Assisted private val rowConfig: HomeRowConfig, ) : ViewModel() { @AssistedFactory interface Factory { fun create( - title: String, + title: StringProvider, rowConfig: HomeRowConfig, ): HomeRowGridViewModel } @@ -155,7 +157,16 @@ class HomeRowGridViewModel } } catch (ex: Exception) { 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( - val loading: HomeRowLoadingState = HomeRowLoadingState.Pending(""), + val loading: HomeRowLoadingState = HomeRowLoadingState.Pending(StringStringProvider("")), ) @OptIn(ExperimentalFoundationApi::class) @@ -268,7 +279,7 @@ fun HomeRowGrid( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, ) { - GridTitle(destination.title) + GridTitle(destination.title.getString()) when (val st = state.loading) { is HomeRowLoadingState.Error -> { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index a6a77e23..913ea3e5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -71,6 +71,7 @@ import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.theme.WholphinTheme 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.DataLoadingState import com.github.damontecres.wholphin.util.DiscoverRequestType @@ -385,7 +386,7 @@ fun PersonPageContent( DiscoverRow( row = DiscoverRowData( - stringResource(R.string.discover), + ResStringProvider(R.string.discover), DataLoadingState.Success(discovered), DiscoverRequestType.UNKNOWN, ), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt index b2876a09..c79eada4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt @@ -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.toServerString 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.ExceptionHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -212,7 +213,7 @@ class CollectionViewModel val jobs = typesInCollection.map { type -> async(Dispatchers.IO) { - val title = context.getString(formatTypeName(type)) + val title = ResStringProvider(formatTypeName(type)) val result = try { val pager = fetchItems(sort, filter, listOf(type)) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt index 85743e3d..5a520c48 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -79,6 +80,7 @@ fun DiscoverMovieDetails( creationCallback = { it.create(destination.item) }, ), ) { + val resources = LocalResources.current val context = LocalContext.current LifecycleResumeEffect(Unit) { viewModel.init() @@ -160,7 +162,7 @@ fun DiscoverMovieDetails( overviewOnClick = { overviewDialog = ItemDetailsDialogInfo( - title = movie.title ?: context.getString(R.string.unknown), + title = movie.title ?: resources.getString(R.string.unknown), overview = movie.overview, genres = movie.genres?.mapNotNull { it.name }.orEmpty(), files = listOf(), @@ -244,7 +246,6 @@ fun DiscoverMovieDetailsContent( onLongClickSimilar: (Int, DiscoverItem) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val scope = rememberCoroutineScope() var position by rememberInt(0) val focusRequesters = remember { List(RECOMMENDED_ROW + 1) { FocusRequester() } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index 4b6a6200..609bd93a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -1,6 +1,6 @@ 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.Box 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.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -83,6 +84,7 @@ fun DiscoverSeriesDetails( ), ) { val context = LocalContext.current + val resources = LocalResources.current val loading by viewModel.loading.observeAsState(LoadingState.Loading) val item by viewModel.tvSeries.observeAsState() @@ -147,7 +149,7 @@ fun DiscoverSeriesDetails( overviewOnClick = { overviewDialog = ItemDetailsDialogInfo( - title = item.name ?: context.getString(R.string.unknown), + title = item.name ?: resources.getString(R.string.unknown), overview = item.overview, genres = item.genres?.mapNotNull { it.name }.orEmpty(), files = listOf(), @@ -494,7 +496,7 @@ fun DiscoverSeriesDetailsHeader( } fun buildDialogForSeason( - context: Context, + resources: Resources, s: BaseItem, onClickItem: (BaseItem) -> Unit, markPlayed: (Boolean) -> Unit, @@ -503,26 +505,26 @@ fun buildDialogForSeason( val items = buildList { add( - DialogItem(context.getString(R.string.go_to), Icons.Default.PlayArrow) { + DialogItem(resources.getString(R.string.go_to), Icons.Default.PlayArrow) { onClickItem.invoke(s) }, ) if (s.data.userData?.played == true) { 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) }, ) } else { 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) }, ) } add( DialogItem( - context.getString(R.string.play), + resources.getString(R.string.play), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), ) { @@ -531,7 +533,7 @@ fun buildDialogForSeason( ) add( DialogItem( - context.getString(R.string.shuffle), + resources.getString(R.string.shuffle), R.string.fa_shuffle, ) { onClickPlay.invoke(true) @@ -539,7 +541,7 @@ fun buildDialogForSeason( ) } return DialogParams( - title = s.name ?: context.getString(R.string.tv_season), + title = s.name ?: resources.getString(R.string.tv_season), fromLongClick = true, items = items, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index e249a0d2..82b0f0ce 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -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.nav.Destination 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.DiscoverRequestType import com.github.damontecres.wholphin.util.ExceptionHandler @@ -478,7 +479,7 @@ fun MovieDetailsContent( DiscoverRow( row = DiscoverRowData( - stringResource(R.string.discover), + ResStringProvider(R.string.discover), DataLoadingState.Success(discovered), type = DiscoverRequestType.UNKNOWN, ), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 18a539c4..bc735720 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -1,6 +1,6 @@ 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.fadeIn 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.nav.Destination 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.DiscoverRequestType import com.github.damontecres.wholphin.util.ExceptionHandler @@ -644,7 +645,7 @@ fun SeriesDetailsContent( DiscoverRow( row = DiscoverRowData( - stringResource(R.string.discover), + ResStringProvider(R.string.discover), DataLoadingState.Success(discovered), type = DiscoverRequestType.UNKNOWN, ), @@ -726,7 +727,7 @@ fun SeriesDetailsHeader( } fun buildDialogForSeason( - context: Context, + resources: Resources, s: BaseItem, canDelete: Boolean, onClickItem: (BaseItem) -> Unit, @@ -737,26 +738,26 @@ fun buildDialogForSeason( val items = buildList { add( - DialogItem(context.getString(R.string.go_to), Icons.Default.PlayArrow) { + DialogItem(resources.getString(R.string.go_to), Icons.Default.PlayArrow) { onClickItem.invoke(s) }, ) if (s.data.userData?.played == true) { 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) }, ) } else { 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) }, ) } add( DialogItem( - context.getString(R.string.play), + resources.getString(R.string.play), Icons.Default.PlayArrow, iconColor = Color.Green.copy(alpha = .8f), ) { @@ -765,7 +766,7 @@ fun buildDialogForSeason( ) add( DialogItem( - context.getString(R.string.shuffle), + resources.getString(R.string.shuffle), R.string.fa_shuffle, ) { onClickPlay.invoke(true) @@ -774,7 +775,7 @@ fun buildDialogForSeason( if (canDelete) { add( DialogItem( - context.getString(R.string.delete), + resources.getString(R.string.delete), Icons.Default.Delete, iconColor = Color.Red.copy(alpha = .8f), ) { @@ -784,7 +785,7 @@ fun buildDialogForSeason( } } return DialogParams( - title = s.name ?: context.getString(R.string.tv_season), + title = s.name ?: resources.getString(R.string.tv_season), fromLongClick = true, items = items, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRow.kt index 039ef566..bbf1dc18 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverRow.kt @@ -59,7 +59,7 @@ fun DiscoverRow( modifier = modifier, ) { Text( - text = row.title, + text = row.title.getString(), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onBackground, ) @@ -73,7 +73,7 @@ fun DiscoverRow( is DataLoadingState.Success> -> { DiscoverItemRow( - title = row.title, + title = row.title.getString(), items = state.data, onClickItem = onClickItem, onLongClickItem = onLongClickItem, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt index 321cc76c..6751a154 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt @@ -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.rememberPosition 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.StringProvider import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.DiscoverRequestType import com.google.common.cache.CacheBuilder @@ -77,7 +80,7 @@ class SeerrDiscoverViewModel this.copy( movies = DiscoverRowData( - context.getString(R.string.movies), + ResStringProvider(R.string.movies), it, DiscoverRequestType.DISCOVER_MOVIES, ), @@ -87,7 +90,7 @@ class SeerrDiscoverViewModel this.copy( tv = DiscoverRowData( - context.getString(R.string.tv_shows), + ResStringProvider(R.string.tv_shows), it, DiscoverRequestType.DISCOVER_TV, ), @@ -97,7 +100,7 @@ class SeerrDiscoverViewModel this.copy( trending = DiscoverRowData( - context.getString(R.string.trending), + ResStringProvider(R.string.trending), it, DiscoverRequestType.TRENDING, ), @@ -107,7 +110,7 @@ class SeerrDiscoverViewModel this.copy( upcomingMovies = DiscoverRowData( - context.getString(R.string.upcoming_movies), + ResStringProvider(R.string.upcoming_movies), it, DiscoverRequestType.UPCOMING_MOVIES, ), @@ -117,7 +120,7 @@ class SeerrDiscoverViewModel this.copy( upcomingTv = DiscoverRowData( - context.getString(R.string.upcoming_tv), + ResStringProvider(R.string.upcoming_tv), it, DiscoverRequestType.UPCOMING_TV, ), @@ -214,12 +217,17 @@ class SeerrDiscoverViewModel } data class DiscoverRowData( - val title: String, + val title: StringProvider, val items: DataLoadingState>, val type: DiscoverRequestType, ) { companion object { - val EMPTY = DiscoverRowData("", DataLoadingState.Pending, DiscoverRequestType.UNKNOWN) + val EMPTY = + DiscoverRowData( + EmptyStringProvider, + DataLoadingState.Pending, + DiscoverRequestType.UNKNOWN, + ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 816fcc25..cfd0de03 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -381,7 +381,7 @@ fun HomePageContent( is HomeRowLoadingState.Pending, -> { FocusableItemRow( - title = r.title, + title = r.title.getString(), subtitle = stringResource(R.string.loading), modifier = Modifier.animateItem(), ) @@ -389,7 +389,7 @@ fun HomePageContent( is HomeRowLoadingState.Error -> { FocusableItemRow( - title = r.title, + title = r.title.getString(), subtitle = r.localizedMessage, isError = true, modifier = Modifier.animateItem(), @@ -400,7 +400,7 @@ fun HomePageContent( if (row.items.isNotEmpty()) { val viewOptions = row.viewOptions ItemRow( - title = row.title, + title = row.title.getString(), items = row.items, onClickItem = remember(rowIndex, onClickItem) { @@ -502,7 +502,7 @@ fun HomePageContent( ) } else if (showEmptyRows) { FocusableItemRow( - title = r.title, + title = r.title.getString(), subtitle = stringResource(R.string.no_results), modifier = Modifier.animateItem(), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index d3722331..f6866b12 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -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.launchIO 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.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingState @@ -103,7 +104,9 @@ class HomeViewModel if (refresh) { it.homeRows } else { - List(settings.rows.size) { HomeRowLoadingState.Pending("") } + List(settings.rows.size) { + HomeRowLoadingState.Pending(EmptyStringProvider) + } }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt index 53f11aac..e46be8bd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt @@ -221,7 +221,7 @@ fun HomeSettingsPage( } } HomeRowSettings( - title = row.title, + title = row.title.getString(), preferenceOptions = preferenceOptions, viewOptions = row.config.viewOptions, defaultViewOptions = defaultViewOptions, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt index 9eadc615..438e671f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsRowList.kt @@ -209,7 +209,7 @@ fun HomeRowConfigContent( ) { HomeSettingsListItem( selected = false, - headlineText = config.title, + headlineText = config.title.getString(), onClick = onClick, modifier = Modifier.weight(1f), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt index c4874aa0..6cf01c24 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt @@ -36,6 +36,10 @@ import com.github.damontecres.wholphin.services.tvAccess import com.github.damontecres.wholphin.ui.AspectRatio import com.github.damontecres.wholphin.ui.launchIO 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.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel @@ -222,7 +226,7 @@ class HomeSettingsViewModel MetaRowType.CONTINUE_WATCHING -> { HomeRowConfigDisplay( id = id, - title = context.getString(R.string.continue_watching), + title = ResStringProvider(R.string.continue_watching), config = ContinueWatching(), ) } @@ -230,7 +234,7 @@ class HomeSettingsViewModel MetaRowType.NEXT_UP -> { HomeRowConfigDisplay( id = id, - title = context.getString(R.string.next_up), + title = ResStringProvider(R.string.next_up), config = NextUp(), ) } @@ -238,7 +242,7 @@ class HomeSettingsViewModel MetaRowType.COMBINED_CONTINUE_WATCHING -> { HomeRowConfigDisplay( id = id, - title = context.getString(R.string.combine_continue_next), + title = ResStringProvider(R.string.combine_continue_next), config = ContinueWatchingCombined(), ) } @@ -291,7 +295,12 @@ class HomeSettingsViewModel when (rowType) { LibraryRowType.RECENTLY_ADDED -> { val title = - library.name.let { context.getString(R.string.recently_added_in, it) } + library.name.let { + ResArgStringProvider( + R.string.recently_added_in, + it, + ) + } HomeRowConfigDisplay( id = id, title = title, @@ -302,7 +311,7 @@ class HomeSettingsViewModel LibraryRowType.RECENTLY_RELEASED -> { val title = library.name.let { - context.getString( + ResArgStringProvider( R.string.recently_released_in, it, ) @@ -315,7 +324,8 @@ class HomeSettingsViewModel } 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( id = id, title = title, @@ -325,7 +335,7 @@ class HomeSettingsViewModel LibraryRowType.STUDIOS -> { val title = - library.name.let { context.getString(R.string.studios_in, it) } + library.name.let { ResArgStringProvider(R.string.studios_in, it) } HomeRowConfigDisplay( id = id, title = title, @@ -335,7 +345,12 @@ class HomeSettingsViewModel LibraryRowType.SUGGESTIONS -> { val title = - library.name.let { context.getString(R.string.suggestions_for, it) } + library.name.let { + ResArgStringProvider( + R.string.suggestions_for, + it, + ) + } HomeRowConfigDisplay( id = id, title = title, @@ -344,7 +359,7 @@ class HomeSettingsViewModel } LibraryRowType.TV_CHANNELS -> { - val title = context.getString(R.string.channels) + val title = ResStringProvider(R.string.channels) HomeRowConfigDisplay( id = id, title = title, @@ -356,7 +371,7 @@ class HomeSettingsViewModel } LibraryRowType.TV_PROGRAMS -> { - val title = context.getString(R.string.watch_live) + val title = ResStringProvider(R.string.watch_live) HomeRowConfigDisplay( id = id, title = title, @@ -365,7 +380,7 @@ class HomeSettingsViewModel } LibraryRowType.RECENTLY_RECORDED -> { - val title = context.getString(R.string.recently_recorded) + val title = ResStringProvider(R.string.recently_recorded) HomeRowConfigDisplay( id = id, title = title, @@ -396,9 +411,9 @@ class HomeSettingsViewModel HomeRowConfigDisplay( id = id, title = - context.getString( + ResProviderStringProvider( R.string.favorite_items, - context.getString(favoriteOptions[type]!!), + ResStringProvider(favoriteOptions[type]!!), ), config = HomeRowConfig.Favorite(type), ) @@ -420,7 +435,7 @@ class HomeSettingsViewModel val newRow = HomeRowConfigDisplay( id = id, - title = parent.name ?: "", + title = StringStringProvider(parent.name ?: ""), config = HomeRowConfig.ByParent( parentId = parent.id, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index 213527db..761978ef 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.ui.nav -import androidx.annotation.StringRes import androidx.navigation3.runtime.NavKey import com.github.damontecres.wholphin.data.model.BaseItem 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.detail.series.SeasonEpisodeIds 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.RequestHandler import com.github.damontecres.wholphin.util.SEERR_PAGE_SIZE @@ -110,8 +110,7 @@ sealed class Destination( @Serializable data class ItemGrid( - val title: String?, - @param:StringRes val titleRes: Int?, + val title: StringProvider, @Contextual val request: T, val requestHandler: RequestHandler, val initialPosition: Int = 0, @@ -120,7 +119,7 @@ sealed class Destination( @Serializable data class MoreHomeRow( - val title: String, + val title: StringProvider, val config: HomeRowConfig, val initialPosition: Int, ) : Destination(false) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 4a73afd9..f6007f55 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -509,14 +509,15 @@ class PlaybackViewModel it } }?.map { - SimpleMediaStream.from(context, it, true) + // TODO should use a string provider instead + SimpleMediaStream.from(context.resources, it, true) }.orEmpty() val audioStreams = mediaSource.mediaStreams ?.filter { it.type == MediaStreamType.AUDIO } ?.map { - SimpleMediaStream.from(context, it, true) + SimpleMediaStream.from(context.resources, it, true) } // ?.sortedWith(compareBy { it.language }.thenByDescending { it.channels }) .orEmpty() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt index d430258a..04699005 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt @@ -1,6 +1,6 @@ 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.util.StreamFormatting.mediaStreamDisplayTitle import org.jellyfin.sdk.model.api.MediaStream @@ -17,14 +17,14 @@ data class SimpleMediaStream( ) { companion object { fun from( - context: Context, + resources: Resources, mediaStream: MediaStream, includeFlags: Boolean = true, ): SimpleMediaStream = SimpleMediaStream( index = mediaStream.index, streamTitle = mediaStream.title?.takeIf { it.isNotNullOrBlank() }, - displayTitle = mediaStreamDisplayTitle(context, mediaStream, includeFlags), + displayTitle = mediaStreamDisplayTitle(resources, mediaStream, includeFlags), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt index b2e82eae..571d5210 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt @@ -155,7 +155,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( it.copy( subtitleStreams = subtitlesStreams.map { - SimpleMediaStream.from(context, it, true) + SimpleMediaStream.from(context.resources, it, true) }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/LocaleChoiceDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/LocaleChoiceDialog.kt new file mode 100644 index 00000000..0ac88d55 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/LocaleChoiceDialog.kt @@ -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 = _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 { + val id = + context.resources.getIdentifier( + "_generated_res_locale_config", + "xml", + context.packageName, + ) + return if (id != 0) { + try { + val locales = mutableListOf() + // 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 = 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), + ) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index 945523c9..67b5733e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -82,6 +82,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Locale @Composable fun PreferencesContent( @@ -109,6 +110,7 @@ fun PreferencesContent( val seerrConnection by viewModel.seerrConnection.collectAsState() var seerrDialogMode by remember { mutableStateOf(SeerrDialogMode.None) } var showQuickConnectDialog by remember { mutableStateOf(false) } + var showLocaleChoiceDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.preferenceDataStore.data.collect { @@ -255,6 +257,12 @@ fun PreferencesContent( pref as AppPreference item { val interactionSource = remember { MutableInteractionSource() } + val focusModifier = + Modifier + .ifElse( + groupIndex == focusedIndex.first && prefIndex == focusedIndex.second, + Modifier.focusRequester(focusRequester), + ) val focused = interactionSource.collectIsFocusedAsState().value LaunchedEffect(focused) { if (focused) { @@ -275,12 +283,7 @@ fun PreferencesContent( }, summary = installedVersion.toString(), interactionSource = interactionSource, - modifier = - Modifier - .ifElse( - groupIndex == focusedIndex.first && prefIndex == focusedIndex.second, - Modifier.focusRequester(focusRequester), - ), + modifier = focusModifier, ) } @@ -317,12 +320,7 @@ fun PreferencesContent( null }, interactionSource = interactionSource, - modifier = - Modifier - .ifElse( - groupIndex == focusedIndex.first && prefIndex == focusedIndex.second, - Modifier.focusRequester(focusRequester), - ), + modifier = focusModifier, ) } @@ -348,7 +346,7 @@ fun PreferencesContent( updateCache = true } }, - modifier = Modifier, + modifier = focusModifier, summary = summary, onLongClick = {}, interactionSource = interactionSource, @@ -359,7 +357,7 @@ fun PreferencesContent( NavDrawerPreference( title = stringResource(pref.title), summary = pref.summary(context, null), - modifier = Modifier, + modifier = focusModifier, interactionSource = interactionSource, ) } @@ -370,7 +368,7 @@ fun PreferencesContent( onClick = { viewModel.sendAppLogs() }, - modifier = Modifier, + modifier = focusModifier, summary = pref.summary(context, null), onLongClick = {}, interactionSource = interactionSource, @@ -383,7 +381,7 @@ fun PreferencesContent( onClick = { viewModel.resetSubtitleSettings() }, - modifier = Modifier, + modifier = focusModifier, summary = pref.summary(context, null), onLongClick = {}, interactionSource = interactionSource, @@ -400,6 +398,7 @@ fun PreferencesContent( ChoicePreference( title = stringResource(pref.title), summary = stringResource(summary), + interactionSource = interactionSource, possibleValues = listOf( stringResource(R.string.none), @@ -429,6 +428,7 @@ fun PreferencesContent( } } }, + modifier = focusModifier, ) } @@ -456,7 +456,7 @@ fun PreferencesContent( } } }, - modifier = Modifier, + modifier = focusModifier, summary = when (seerrConnection) { is SeerrConnectionStatus.Error -> { @@ -487,7 +487,7 @@ fun PreferencesContent( showQuickConnectDialog = true } }, - modifier = Modifier, + modifier = focusModifier, summary = pref.summary(context, null), onLongClick = {}, interactionSource = interactionSource, @@ -500,7 +500,7 @@ fun PreferencesContent( onClick = { viewModel.screensaverService.start() }, - modifier = Modifier, + modifier = focusModifier, summary = pref.summary(context, null), onLongClick = {}, interactionSource = interactionSource, @@ -516,6 +516,7 @@ fun PreferencesContent( ChoicePreference( title = stringResource(pref.title), summary = players[selectedIndex].name, + interactionSource = interactionSource, possibleValues = players, selectedIndex = selectedIndex, onValueChange = { index -> @@ -543,6 +544,25 @@ fun PreferencesContent( 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, - modifier = - Modifier - .ifElse( - groupIndex == focusedIndex.first && prefIndex == focusedIndex.second, - Modifier.focusRequester(focusRequester), - ), + modifier = focusModifier, ) } } @@ -721,6 +736,11 @@ fun PreferencesContent( } } } + if (showLocaleChoiceDialog) { + LocaleChoiceDialog( + onDismissRequest = { showLocaleChoiceDialog = false }, + ) + } } @Composable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt index 15d35b3b..4f1a592d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt @@ -1,6 +1,6 @@ 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.ui.isNotNullOrBlank import com.github.damontecres.wholphin.util.languageName @@ -44,7 +44,7 @@ object StreamFormatting { } fun formatVideoRange( - context: Context, + resources: Resources, videoRange: VideoRange?, type: VideoRangeType?, doviTitle: String?, @@ -58,7 +58,7 @@ object StreamFormatting { VideoRange.HDR -> { if (doviTitle.isNotNullOrBlank()) { - context.getString(R.string.dolby_vision) + resources.getString(R.string.dolby_vision) } else { when (type) { VideoRangeType.UNKNOWN, @@ -76,40 +76,40 @@ object StreamFormatting { VideoRangeType.DOVI_WITH_HDR10, VideoRangeType.DOVI_WITH_HLG, VideoRangeType.DOVI_WITH_SDR, - -> context.getString(R.string.dolby_vision) + -> resources.getString(R.string.dolby_vision) } } } } fun formatAudioCodec( - context: Context, + resources: Resources, codec: String?, profile: String?, ): String? = when { profile?.contains("Dolby Atmos", true) == true -> { - context.getString(R.string.dolby_atmos) + resources.getString(R.string.dolby_atmos) } profile?.contains("DTS", true) == true -> { when { - profile.contains("X", true) -> context.getString(R.string.dts_x) - profile.contains("MA", true) -> context.getString(R.string.dts_hd_ma) - profile.contains("HD", true) -> context.getString(R.string.dts_hd) - else -> context.getString(R.string.dts) + profile.contains("X", true) -> resources.getString(R.string.dts_x) + profile.contains("MA", true) -> resources.getString(R.string.dts_hd_ma) + profile.contains("HD", true) -> resources.getString(R.string.dts_hd) + else -> resources.getString(R.string.dts) } } else -> { 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.OPUS, @@ -141,7 +141,7 @@ object StreamFormatting { } fun mediaStreamDisplayTitle( - context: Context, + resources: Resources, stream: MediaStream, includeFlags: Boolean, ): String { @@ -149,7 +149,7 @@ object StreamFormatting { buildList { add(languageName(stream.language)) if (stream.type == MediaStreamType.AUDIO) { - add(formatAudioCodec(context, stream.codec, stream.profile)) + add(formatAudioCodec(resources, stream.codec, stream.profile)) add(stream.channelLayout) } else if (stream.type == MediaStreamType.SUBTITLE) { "SDH".takeIf { stream.isHearingImpaired }?.let(::add) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/StringProvider.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/StringProvider.kt new file mode 100644 index 00000000..6c3e7bca --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/StringProvider.kt @@ -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, +) : 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()) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt index 97308113..585fff4b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt @@ -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.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowViewOptions +import com.github.damontecres.wholphin.ui.util.StringProvider /** * Generic state for loading something from the API @@ -48,28 +49,28 @@ sealed interface RowLoadingState { } sealed interface HomeRowLoadingState { - val title: String + val title: StringProvider val completed: Boolean get() = this is Success || this is Error data class Pending( - override val title: String, + override val title: StringProvider, ) : HomeRowLoadingState data class Loading( - override val title: String, + override val title: StringProvider, ) : HomeRowLoadingState data class Success( - override val title: String, + override val title: StringProvider, val items: List, val viewOptions: HomeRowViewOptions = HomeRowViewOptions(), val rowType: HomeRowConfig? = null, ) : HomeRowLoadingState data class Error( - override val title: String, + override val title: StringProvider, val message: String? = null, val exception: Throwable? = null, ) : HomeRowLoadingState { diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 00000000..467b3efe --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d5e4d22..6c18fa1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -793,6 +793,7 @@ For intros, credits, etc Play with Transcoding + User interface language Ratings display Customize which to display Profile protection