mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Support configuring interface language per user (#1406)
## Description Adds a setting to configure the app's language/locale. It is configured per user. This settings has no impact on audio or subtitles language choices. Only languages Wholphin has translations for can be used. If you would like to contribute new translations, check out https://translate.codeberg.org/projects/wholphin/! Dev notes: This doesn't exactly follow Android's recommendations because the [guidelines are for per-app](https://developer.android.com/guide/topics/resources/app-languages) and not users within the app. ### Related issues Closes #513 Closes #669 ### Testing - Emulator API 34 - Nvidia shield API 30 - Fire TV OS 7 API 28 ## Screenshots <img width="1280" height="720" alt="language Large" src="https://github.com/user-attachments/assets/bf1c0962-8a36-4012-aed9-eab564fc1114" /> ## AI or LLM usage None
This commit is contained in:
parent
625cdcb4a3
commit
903c45dfb8
43 changed files with 1501 additions and 418 deletions
|
|
@ -84,6 +84,15 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="false" />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -704,6 +704,13 @@ sealed interface AppPreference<Pref, T> {
|
|||
summary = R.string.customize_home_summary,
|
||||
)
|
||||
|
||||
val UserInterfaceLanguage =
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.user_interface_language,
|
||||
getter = { },
|
||||
setter = { prefs, _ -> prefs },
|
||||
)
|
||||
|
||||
val SendCrashReports =
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.send_crash_reports,
|
||||
|
|
@ -1110,6 +1117,7 @@ val basicPreferences =
|
|||
AppPreference.ProtectProfilePreference,
|
||||
AppPreference.CustomizeHome,
|
||||
AppPreference.UserPinnedNavDrawerItems,
|
||||
AppPreference.UserInterfaceLanguage,
|
||||
),
|
||||
),
|
||||
PreferenceGroup(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.damontecres.wholphin.BuildConfig
|
||||
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import dagger.hilt.android.qualifiers.ActivityContext
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Listens for JF user switching in the app to also switch other settings like Seerr user/server
|
||||
*/
|
||||
@ActivityScoped
|
||||
class UserSwitchListener
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ActivityContext private val context: Context,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val seerrServerRepository: SeerrServerRepository,
|
||||
private val seerrServerDao: SeerrServerDao,
|
||||
private val seerrApi: SeerrApi,
|
||||
private val homeSettingsService: HomeSettingsService,
|
||||
) {
|
||||
init {
|
||||
context as AppCompatActivity
|
||||
context.lifecycleScope.launchDefault {
|
||||
serverRepository.currentUser.asFlow().collect { user ->
|
||||
Timber.d("New user")
|
||||
seerrServerRepository.clear()
|
||||
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }
|
||||
if (user != null) {
|
||||
switchUser(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun switchUser(user: JellyfinUser) =
|
||||
supervisorScope {
|
||||
// Switch the locale to either the user's choice or the system default (empty)
|
||||
val localeList =
|
||||
user.uiLanguage?.let { LocaleListCompat.forLanguageTags(it) }
|
||||
?: LocaleListCompat.getEmptyLocaleList()
|
||||
Timber.i("Switching locale to %s", localeList)
|
||||
withContext(Dispatchers.Main) {
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
|
||||
// Check for home settings
|
||||
launchIO {
|
||||
homeSettingsService.loadCurrentSettings(user.id)
|
||||
}
|
||||
if (BuildConfig.DISCOVER_ENABLED) {
|
||||
// Check for seerr server
|
||||
launchIO {
|
||||
seerrServerDao
|
||||
.getUsersByJellyfinUser(user.rowId)
|
||||
.lastOrNull()
|
||||
?.let { seerrUser ->
|
||||
val server =
|
||||
seerrServerDao.getServer(seerrUser.serverId)?.server
|
||||
if (server != null) {
|
||||
Timber.i("Found a seerr user & server")
|
||||
try {
|
||||
seerrApi.update(server.url, seerrUser.credential)
|
||||
val userConfig =
|
||||
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
||||
seerrLogin(
|
||||
seerrApi.api,
|
||||
seerrUser.authMethod,
|
||||
seerrUser.username,
|
||||
seerrUser.password,
|
||||
)
|
||||
} else {
|
||||
seerrApi.api.usersApi.authMeGet()
|
||||
}
|
||||
seerrServerRepository.set(
|
||||
server,
|
||||
seerrUser,
|
||||
userConfig,
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(
|
||||
ex,
|
||||
"Error logging into %s",
|
||||
server.url,
|
||||
)
|
||||
seerrServerRepository.error(server, seerrUser, ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
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<DialogParams?>(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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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<MediaSourceInfo>,
|
||||
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<MediaStream>,
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<Pair<String, String>> =
|
||||
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<Pair<String, String>> =
|
||||
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<Pair<String, String>> =
|
||||
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<Pair<String, String>> =
|
||||
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)) }
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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() } }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<List<DiscoverItem>> -> {
|
||||
DiscoverItemRow(
|
||||
title = row.title,
|
||||
title = row.title.getString(),
|
||||
items = state.data,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@ import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
|||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.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<List<DiscoverItem>>,
|
||||
val type: DiscoverRequestType,
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = DiscoverRowData("", DataLoadingState.Pending, DiscoverRequestType.UNKNOWN)
|
||||
val EMPTY =
|
||||
DiscoverRowData(
|
||||
EmptyStringProvider,
|
||||
DataLoadingState.Pending,
|
||||
DiscoverRequestType.UNKNOWN,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ fun HomePageContent(
|
|||
is HomeRowLoadingState.Pending,
|
||||
-> {
|
||||
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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ fun HomeSettingsPage(
|
|||
}
|
||||
}
|
||||
HomeRowSettings(
|
||||
title = row.title,
|
||||
title = row.title.getString(),
|
||||
preferenceOptions = preferenceOptions,
|
||||
viewOptions = row.config.viewOptions,
|
||||
defaultViewOptions = defaultViewOptions,
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ fun HomeRowConfigContent(
|
|||
) {
|
||||
HomeSettingsListItem(
|
||||
selected = false,
|
||||
headlineText = config.title,
|
||||
headlineText = config.title.getString(),
|
||||
onClick = onClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ import com.github.damontecres.wholphin.services.tvAccess
|
|||
import com.github.damontecres.wholphin.ui.AspectRatio
|
||||
import com.github.damontecres.wholphin.ui.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,
|
||||
|
|
|
|||
|
|
@ -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<T>(
|
||||
val title: String?,
|
||||
@param:StringRes val titleRes: Int?,
|
||||
val title: StringProvider,
|
||||
@Contextual val request: T,
|
||||
val requestHandler: RequestHandler<T>,
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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<AudioStream> { it.language }.thenByDescending { it.channels })
|
||||
.orEmpty()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
|
|||
it.copy(
|
||||
subtitleStreams =
|
||||
subtitlesStreams.map {
|
||||
SimpleMediaStream.from(context, it, true)
|
||||
SimpleMediaStream.from(context.resources, it, true)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
package com.github.damontecres.wholphin.ui.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.XmlResourceParser
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopupContent
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LocaleChoiceViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val serverRepository: ServerRepository,
|
||||
) : ViewModel() {
|
||||
fun changeLocale(locale: Locale) {
|
||||
viewModelScope.launchDefault {
|
||||
serverRepository.current.value?.let {
|
||||
Timber.i("Updating user's locale to %s", locale.toLanguageTag())
|
||||
val newUser = it.user.copy(uiLanguage = locale.toLanguageTag())
|
||||
serverRepository.changeUser(it.server, newUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(LocaleChoiceState())
|
||||
val state: StateFlow<LocaleChoiceState> = _state
|
||||
|
||||
init {
|
||||
viewModelScope.launchDefault {
|
||||
serverRepository.currentUser.value?.let {
|
||||
val availableLocales = extractAvailableLocales()
|
||||
Timber.v("availableLocales=%s", availableLocales)
|
||||
_state.update {
|
||||
it.copy(
|
||||
loading = LoadingState.Success,
|
||||
availableLocales = availableLocales,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launchDefault {
|
||||
serverRepository.currentUser.asFlow().collectLatest { user ->
|
||||
val userLocale = user?.uiLanguage?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
||||
_state.update { it.copy(userLocale = userLocale) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available locales that can be used.
|
||||
*
|
||||
* Defaults to `context.assets.locales` (all locales) if can't find app specific ones
|
||||
*/
|
||||
private fun extractAvailableLocales(): List<Locale> {
|
||||
val id =
|
||||
context.resources.getIdentifier(
|
||||
"_generated_res_locale_config",
|
||||
"xml",
|
||||
context.packageName,
|
||||
)
|
||||
return if (id != 0) {
|
||||
try {
|
||||
val locales = mutableListOf<Locale>()
|
||||
// This is kind of a hack, the generated locale config is not available,
|
||||
// programmatically, so this code parses the raw XML
|
||||
context.resources.getXml(id).use { parser ->
|
||||
var eventType: Int = parser.eventType
|
||||
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||
if (eventType == XmlResourceParser.START_TAG) {
|
||||
val tagName: String? = parser.name
|
||||
|
||||
// Check for a specific tag name
|
||||
if ("locale" == tagName) {
|
||||
val attr =
|
||||
parser.getAttributeValue(
|
||||
"http://schemas.android.com/apk/res/android",
|
||||
"name",
|
||||
)
|
||||
attr?.let { locales.add(Locale.forLanguageTag(it)) }
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
}
|
||||
locales
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Exception while parsing generated available locales")
|
||||
context.assets.locales.map { Locale.forLanguageTag(it) }
|
||||
}
|
||||
} else {
|
||||
context.assets.locales.map { Locale.forLanguageTag(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LocaleChoiceState(
|
||||
val loading: LoadingState = LoadingState.Pending,
|
||||
val availableLocales: List<Locale> = emptyList(),
|
||||
val userLocale: Locale = Locale.getDefault(),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LocaleChoiceDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
viewModel: LocaleChoiceViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(),
|
||||
) {
|
||||
when (val st = state.loading) {
|
||||
is LoadingState.Error -> {
|
||||
ErrorMessage(st)
|
||||
}
|
||||
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> {
|
||||
LoadingPage()
|
||||
}
|
||||
|
||||
LoadingState.Success -> {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
val dialogItems =
|
||||
remember(state.userLocale, state.availableLocales) {
|
||||
val userLanguage = state.userLocale.toLanguageTag()
|
||||
state.availableLocales.map { locale ->
|
||||
val tag = locale.toLanguageTag()
|
||||
DialogItem(
|
||||
selected = tag == userLanguage,
|
||||
leadingContent = {
|
||||
Box {
|
||||
SelectedLeadingContent(tag == userLanguage)
|
||||
}
|
||||
},
|
||||
headlineContent = {
|
||||
Text(locale.getDisplayName(locale))
|
||||
},
|
||||
supportingContent = {
|
||||
Text(locale.getDisplayName(Locale.ENGLISH))
|
||||
},
|
||||
dismissOnClick = true,
|
||||
onClick = {
|
||||
viewModel.changeLocale(locale)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
DialogPopupContent(
|
||||
title = stringResource(R.string.user_interface_language),
|
||||
dialogItems = dialogItems,
|
||||
waiting = false,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
|||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import 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>(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<AppPreferences, Any>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package com.github.damontecres.wholphin.ui.util
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Flexible way to provide a string for the UI
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface StringProvider {
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun getString(): String
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a string literal
|
||||
*/
|
||||
@Serializable
|
||||
data class StringStringProvider(
|
||||
val str: String,
|
||||
) : StringProvider {
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
override fun getString(): String = str
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an empty string literal
|
||||
*/
|
||||
@Serializable
|
||||
data object EmptyStringProvider : StringProvider {
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
override fun getString(): String = ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a string resource using [stringResource]
|
||||
*/
|
||||
@Serializable
|
||||
data class ResStringProvider(
|
||||
@param:StringRes val stringResId: Int,
|
||||
) : StringProvider {
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
override fun getString() = stringResource(stringResId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a string resource with format arguments using [stringResource]
|
||||
*/
|
||||
@Serializable
|
||||
data class ResArgStringProvider(
|
||||
@param:StringRes val stringResId: Int,
|
||||
val args: Array<String>,
|
||||
) : StringProvider {
|
||||
constructor(stringResId: Int, arg: String) : this(stringResId, arrayOf(arg))
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
override fun getString() = stringResource(stringResId, *args)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ResArgStringProvider) return false
|
||||
|
||||
if (stringResId != other.stringResId) return false
|
||||
if (!args.contentEquals(other.args)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = stringResId
|
||||
result = 31 * result + args.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a string resource with another [StringProvider] format argument using [stringResource]
|
||||
*/
|
||||
@Serializable
|
||||
data class ResProviderStringProvider(
|
||||
@param:StringRes val stringResId: Int,
|
||||
val argProvider: StringProvider,
|
||||
) : StringProvider {
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
override fun getString(): String = stringResource(stringResId, argProvider.getString())
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.util
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.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<BaseItem?>,
|
||||
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 {
|
||||
|
|
|
|||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
unqualifiedResLocale=en-US
|
||||
|
|
@ -793,6 +793,7 @@
|
|||
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
||||
<string name="play_with">Play with</string>
|
||||
<string name="transcoding">Transcoding</string>
|
||||
<string name="user_interface_language">User interface language</string>
|
||||
<string name="display_toggles_title">Ratings display</string>
|
||||
<string name="display_toggles_summary">Customize which to display</string>
|
||||
<string name="profile_protection">Profile protection</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue