Extract strings into resources (#151)

Move all strings into resource files to make future translating easier

No user facing changes in this PR
This commit is contained in:
damontecres 2025-11-03 15:38:12 -05:00 committed by GitHub
parent 40267be633
commit 2840fa3f2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 616 additions and 306 deletions

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.provider.Settings import android.provider.Settings
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.BuildConfig
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -47,9 +48,11 @@ annotation class IoCoroutineScope
object AppModule { object AppModule {
@Provides @Provides
@Singleton @Singleton
fun clientInfo(): ClientInfo = fun clientInfo(
@ApplicationContext context: Context,
): ClientInfo =
ClientInfo( ClientInfo(
name = "Wholphin", name = context.getString(R.string.app_name),
version = BuildConfig.VERSION_NAME, version = BuildConfig.VERSION_NAME,
) )

View file

@ -70,7 +70,11 @@ sealed interface AppPreference<T> {
}, },
summarizer = { value -> summarizer = { value ->
if (value != null) { if (value != null) {
"$value seconds" WholphinApplication.instance.resources.getQuantityString(
R.plurals.seconds,
value.toInt(),
value.toInt(),
)
} else { } else {
null null
} }
@ -95,7 +99,11 @@ sealed interface AppPreference<T> {
}, },
summarizer = { value -> summarizer = { value ->
if (value != null) { if (value != null) {
"$value seconds" WholphinApplication.instance.resources.getQuantityString(
R.plurals.seconds,
value.toInt(),
value.toInt(),
)
} else { } else {
null null
} }
@ -137,7 +145,14 @@ sealed interface AppPreference<T> {
setter = { prefs, value -> setter = { prefs, value ->
prefs.updatePlaybackPreferences { controllerTimeoutMs = value } prefs.updatePlaybackPreferences { controllerTimeoutMs = value }
}, },
summarizer = { value -> value?.let { "${value / 1000.0} seconds" } }, summarizer = { value ->
value?.let {
WholphinApplication.instance.getString(
R.string.decimal_seconds,
value / 1000.0,
)
}
},
) )
val SeekBarSteps = val SeekBarSteps =
@ -243,7 +258,7 @@ sealed interface AppPreference<T> {
}, },
summarizer = { value -> summarizer = { value ->
if (value == 0L) { if (value == 0L) {
"Disabled" WholphinApplication.instance.getString(R.string.disabled)
} else { } else {
"${value}s" "${value}s"
} }
@ -262,10 +277,16 @@ sealed interface AppPreference<T> {
prefs.updatePlaybackPreferences { autoPlayNextDelaySeconds = value } prefs.updatePlaybackPreferences { autoPlayNextDelaySeconds = value }
}, },
summarizer = { value -> summarizer = { value ->
if (value == 0L) { if (value == null) {
"Immediate" ""
} else if (value == 0L) {
WholphinApplication.instance.getString(R.string.immediate)
} else { } else {
"$value seconds" WholphinApplication.instance.resources.getQuantityString(
R.plurals.seconds,
value.toInt(),
value.toInt(),
)
} }
}, },
) )
@ -284,10 +305,16 @@ sealed interface AppPreference<T> {
} }
}, },
summarizer = { value -> summarizer = { value ->
if (value == 0L) { if (value == null) {
"Disabled" ""
} else if (value == 0L) {
WholphinApplication.instance.getString(R.string.disabled)
} else { } else {
"$value hours" WholphinApplication.instance.resources.getQuantityString(
R.plurals.hours,
value.toInt(),
value.toInt(),
)
} }
}, },
) )
@ -485,7 +512,7 @@ sealed interface AppPreference<T> {
val SkipCommercials = val SkipCommercials =
AppChoicePreference<SkipSegmentBehavior>( AppChoicePreference<SkipSegmentBehavior>(
title = R.string.skip_comercials_behavior, title = R.string.skip_commercials_behavior,
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP, defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
getter = { it.playbackPreferences.skipCommercials }, getter = { it.playbackPreferences.skipCommercials },
setter = { prefs, value -> setter = { prefs, value ->

View file

@ -11,9 +11,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.Chapter
@Composable @Composable
@ -28,7 +30,7 @@ fun ChapterRow(
modifier = modifier, modifier = modifier,
) { ) {
Text( Text(
text = "Chapters", text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )

View file

@ -15,9 +15,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
@ -34,7 +36,7 @@ fun PersonRow(
modifier = modifier, modifier = modifier,
) { ) {
Text( Text(
text = "People", text = stringResource(R.string.people),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.components package com.github.damontecres.wholphin.ui.components
import android.content.Context
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -19,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -26,6 +28,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
@ -46,6 +49,7 @@ import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -65,6 +69,7 @@ class CollectionFolderViewModel
@Inject @Inject
constructor( constructor(
api: ApiClient, api: ApiClient,
@param:ApplicationContext private val context: Context,
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
private val libraryDisplayInfoDao: LibraryDisplayInfoDao, private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
) : ItemViewModel(api) { ) : ItemViewModel(api) {
@ -83,7 +88,7 @@ class CollectionFolderViewModel
viewModelScope.launch( viewModelScope.launch(
LoadingExceptionHandler( LoadingExceptionHandler(
loading, loading,
"Error loading collection $itemId", context.getString(R.string.error_loading_collection, itemId),
) + Dispatchers.IO, ) + Dispatchers.IO,
) { ) {
if (itemId != null) { if (itemId != null) {
@ -292,7 +297,7 @@ fun CollectionFolderGridContent(
showTitle: Boolean = true, showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
) { ) {
val title = item?.name ?: item?.data?.collectionType?.name ?: "Collection" val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection)
val sortOptions = val sortOptions =
when (item?.data?.collectionType) { when (item?.data?.collectionType) {
CollectionType.MOVIES -> MovieSortOptions CollectionType.MOVIES -> MovieSortOptions

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.components package com.github.damontecres.wholphin.ui.components
import android.content.Context
import android.view.KeyEvent import android.view.KeyEvent
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -462,12 +463,13 @@ fun ConfirmDialogContent(
} }
fun chooseVersionParams( fun chooseVersionParams(
context: Context,
sources: List<MediaSourceInfo>, sources: List<MediaSourceInfo>,
onClick: (Int) -> Unit, onClick: (Int) -> Unit,
): DialogParams = ): DialogParams =
DialogParams( DialogParams(
fromLongClick = false, fromLongClick = false,
title = "Play Version", title = context.getString(R.string.choose_stream, context.getString(R.string.version)),
items = items =
sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source -> sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source ->
val videoStream = val videoStream =
@ -485,21 +487,33 @@ fun chooseVersionParams(
}, },
) )
@StringRes
fun resourceFor(type: MediaStreamType): Int =
when (type) {
MediaStreamType.AUDIO -> R.string.audio
MediaStreamType.VIDEO -> R.string.video
MediaStreamType.SUBTITLE -> R.string.subtitles
MediaStreamType.EMBEDDED_IMAGE -> 0
MediaStreamType.DATA -> 0
MediaStreamType.LYRIC -> 0
}
fun chooseStream( fun chooseStream(
context: Context,
streams: List<MediaStream>, streams: List<MediaStream>,
type: MediaStreamType, type: MediaStreamType,
onClick: (Int) -> Unit, onClick: (Int) -> Unit,
): DialogParams = ): DialogParams =
DialogParams( DialogParams(
fromLongClick = false, fromLongClick = false,
title = "Choose ${type.serialName}", // TODO title = context.getString(R.string.choose_stream, context.getString(resourceFor(type))),
items = items =
buildList { buildList {
if (type == MediaStreamType.SUBTITLE) { if (type == MediaStreamType.SUBTITLE) {
add( add(
DialogItem( DialogItem(
headlineContent = { headlineContent = {
Text(text = "None") Text(text = stringResource(R.string.none))
}, },
supportingContent = { supportingContent = {
}, },

View file

@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -50,7 +52,7 @@ fun ErrorMessage(
modifier = modifier.padding(16.dp), modifier = modifier.padding(16.dp),
) { ) {
Text( Text(
text = "An error occurred! Press the button to send logs to your server.", text = stringResource(R.string.compose_error_message_title),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
) )
@ -60,7 +62,7 @@ fun ErrorMessage(
}, },
) { ) {
Text( Text(
text = "Send Logs", text = stringResource(R.string.send_app_logs),
) )
} }
message?.let { message?.let {

View file

@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.ItemRow
@ -70,7 +72,7 @@ fun LoadingRow(
-> ->
LoadingRowPlaceholder( LoadingRowPlaceholder(
title = title, title = title,
message = "Loading...", message = stringResource(R.string.loading),
modifier = modifier, modifier = modifier,
) )
@ -88,7 +90,7 @@ fun LoadingRow(
} else if (showIfEmpty) { } else if (showIfEmpty) {
LoadingRowPlaceholder( LoadingRowPlaceholder(
title = title, title = title,
message = "No results", message = stringResource(R.string.no_results),
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -8,6 +8,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -16,7 +17,6 @@ import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.main.HomeRow import com.github.damontecres.wholphin.ui.main.HomeRow
import com.github.damontecres.wholphin.ui.main.HomeSection
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@ -113,11 +113,11 @@ class RecommendedMovieViewModel
rows.forEach { it.init() } rows.forEach { it.init() }
val homeRows = val homeRows =
listOf( listOf(
HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"), HomeRow(R.string.continue_watching, resumeItems),
HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"), HomeRow(R.string.recently_released, recentlyReleasedItems),
HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), HomeRow(R.string.recently_added, recentlyAddedItems),
HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), HomeRow(R.string.suggestions, suggestedItems),
HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
).filter { it.items.isNotEmpty() } ).filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@RecommendedMovieViewModel.rows.value = homeRows this@RecommendedMovieViewModel.rows.value = homeRows

View file

@ -8,6 +8,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -16,7 +17,6 @@ import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.main.HomeRow import com.github.damontecres.wholphin.ui.main.HomeRow
import com.github.damontecres.wholphin.ui.main.HomeSection
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@ -124,12 +124,12 @@ class RecommendedTvShowViewModel
rows.forEach { it.init() } rows.forEach { it.init() }
val homeRows = val homeRows =
listOf( listOf(
HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"), HomeRow(R.string.continue_watching, resumeItems),
HomeRow(HomeSection.NEXT_UP, nextUpItems, "Next Up"), HomeRow(R.string.next_up, nextUpItems),
HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"), HomeRow(R.string.recently_released, recentlyReleasedItems),
HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"), HomeRow(R.string.recently_added, recentlyAddedItems),
HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"), HomeRow(R.string.suggestions, suggestedItems),
HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"), HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
).filter { it.items.isNotEmpty() } ).filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@RecommendedTvShowViewModel.rows.value = homeRows this@RecommendedTvShowViewModel.rows.value = homeRows

View file

@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.ProvideTextStyle
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.ScrollableDialog import com.github.damontecres.wholphin.ui.components.ScrollableDialog
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
@ -74,24 +76,30 @@ fun MediaSourceInfo(
modifier = modifier, modifier = modifier,
) { ) {
Text( Text(
text = "Name: ${source.name}", text = stringResource(R.string.name) + ": ${source.name}",
) )
Text( Text(
text = "ID: ${source.id}", text = "ID: ${source.id}",
) )
if (showFilePath) { if (showFilePath) {
Text( Text(
text = "Path: ${source.path}", text = stringResource(R.string.path) + ": ${source.path}",
) )
} }
source.size?.let { size -> source.size?.let { size ->
Text( Text(
text = "Size: ${formatBytes(size)}", text = stringResource(R.string.file_size) + ": ${formatBytes(size)}",
) )
} }
source.bitrate?.let { bitrate -> source.bitrate?.let { bitrate ->
Text( Text(
text = "Bitrate: ${formatBytes(bitrate, byteRateSuffixes)}", text =
stringResource(R.string.bitrate) + ": ${
formatBytes(
bitrate,
byteRateSuffixes,
)
}",
) )
} }
source.mediaStreams?.letNotEmpty { streams -> source.mediaStreams?.letNotEmpty { streams ->
@ -109,7 +117,7 @@ fun MediaSourceInfo(
stream.profile?.let(::add) stream.profile?.let(::add)
} }
Text( Text(
text = "Video: " + data.joinToString(" - "), text = stringResource(R.string.video) + ": " + data.joinToString(" - "),
) )
} }
@ -121,10 +129,10 @@ fun MediaSourceInfo(
stream.channelLayout?.let(::add) stream.channelLayout?.let(::add)
stream.bitRate?.let { add(formatBytes(it, byteRateSuffixes)) } stream.bitRate?.let { add(formatBytes(it, byteRateSuffixes)) }
if (stream.audioSpatialFormat != AudioSpatialFormat.NONE) add(stream.audioSpatialFormat.serialName) if (stream.audioSpatialFormat != AudioSpatialFormat.NONE) add(stream.audioSpatialFormat.serialName)
if (stream.isDefault) add("Default") if (stream.isDefault) add(stringResource(R.string.default_track))
} }
Text( Text(
text = "Audio ${index + 1}: " + data.joinToString(" - "), text = stringResource(R.string.audio) + " ${index + 1}: " + data.joinToString(" - "),
) )
} }
@ -133,12 +141,14 @@ fun MediaSourceInfo(
buildList { buildList {
stream.language?.let { add(languageName(it)) } stream.language?.let { add(languageName(it)) }
stream.codec?.let(::add) stream.codec?.let(::add)
if (stream.isDefault) add("Default") if (stream.isDefault) add(stringResource(R.string.default_track))
if (stream.isForced) add("Forced") if (stream.isForced) add(stringResource(R.string.forced_track))
if (stream.isExternal) add("External") if (stream.isExternal) add(stringResource(R.string.external_track))
} }
Text( Text(
text = "Subtitle ${index + 1}: " + data.joinToString(" - "), text =
stringResource(R.string.subtitle) + " ${index + 1}: " +
data.joinToString(" - "),
) )
} }
} }

View file

@ -42,6 +42,7 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Button import androidx.tv.material3.Button
@ -298,7 +299,7 @@ fun CardGrid(
// focusedIndex = -1 // focusedIndex = -1
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Text( Text(
text = "No results", text = stringResource(R.string.no_results),
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
) )
@ -326,6 +327,8 @@ fun CardGrid(
} }
} }
} }
val context = LocalContext.current
val letters = context.getString(R.string.jump_letters)
// Letters // Letters
val currentLetter = val currentLetter =
remember(focusedIndex) { remember(focusedIndex) {
@ -344,10 +347,11 @@ fun CardGrid(
null null
} }
} }
?: LETTERS[0] ?: letters[0]
} }
if (showLetterButtons && pager.isNotEmpty()) { if (showLetterButtons && pager.isNotEmpty()) {
AlphabetButtons( AlphabetButtons(
letters = letters,
currentLetter = currentLetter, currentLetter = currentLetter,
modifier = Modifier.align(Alignment.CenterVertically), modifier = Modifier.align(Alignment.CenterVertically),
letterClicked = { letter -> letterClicked = { letter ->
@ -404,18 +408,18 @@ fun JumpButton(
} }
} }
private const val LETTERS = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@Composable @Composable
fun AlphabetButtons( fun AlphabetButtons(
letters: String,
currentLetter: Char, currentLetter: Char,
letterClicked: (Char) -> Unit, letterClicked: (Char) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val listState = rememberLazyListState() val listState = rememberLazyListState()
val index = LETTERS.indexOf(currentLetter) val index = letters.indexOf(currentLetter)
LaunchedEffect(currentLetter) { LaunchedEffect(currentLetter) {
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
val firstVisibleItemIndex = listState.firstVisibleItemIndex val firstVisibleItemIndex = listState.firstVisibleItemIndex
@ -428,19 +432,19 @@ fun AlphabetButtons(
} }
} }
} }
val focusRequesters = remember { List(LETTERS.length) { FocusRequester() } } val focusRequesters = remember { List(letters.length) { FocusRequester() } }
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = modifier =
modifier.focusProperties { modifier.focusProperties {
onEnter = { onEnter = {
focusRequesters[index.coerceIn(0, LETTERS.length - 1)].tryRequestFocus() focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus()
} }
}, },
) { ) {
items( items(
LETTERS.length, letters.length,
key = { LETTERS[it] }, key = { letters[it] },
) { index -> ) { index ->
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
@ -452,17 +456,17 @@ fun AlphabetButtons(
contentPadding = PaddingValues(2.dp), contentPadding = PaddingValues(2.dp),
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = { onClick = {
letterClicked.invoke(LETTERS[index]) letterClicked.invoke(letters[index])
}, },
) { ) {
val color = val color =
if (!focused && LETTERS[index] == currentLetter) { if (!focused && letters[index] == currentLetter) {
MaterialTheme.colorScheme.tertiary MaterialTheme.colorScheme.tertiary
} else { } else {
LocalContentColor.current LocalContentColor.current
} }
Text( Text(
text = LETTERS[index].toString(), text = letters[index].toString(),
color = color, color = color,
) )
} }

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -33,6 +34,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
@ -96,8 +98,8 @@ fun CollectionFolderLiveTv(
val tabs = val tabs =
listOf( listOf(
TabId("Guide", UUID.randomUUID()), TabId(stringResource(R.string.tv_guide), UUID.randomUUID()),
TabId("DVR Schedule", UUID.randomUUID()), TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()),
) + folders ) + folders
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -52,7 +54,13 @@ fun CollectionFolderMovie(
val rememberedTabIndex = val rememberedTabIndex =
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) } remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
val tabs = listOf("Recommended", "Library", "Collections", "Genres") val tabs =
listOf(
stringResource(R.string.recommended),
stringResource(R.string.library),
stringResource(R.string.collections),
stringResource(R.string.genres),
)
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -52,7 +54,12 @@ fun CollectionFolderTv(
val rememberedTabIndex = val rememberedTabIndex =
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) } remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
val tabs = listOf("Recommended", "Library", "Genres") val tabs =
listOf(
stringResource(R.string.recommended),
stringResource(R.string.library),
stringResource(R.string.genres),
)
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.detail package com.github.damontecres.wholphin.ui.detail
import android.content.Context
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.ArrowForward
@ -36,6 +37,7 @@ import kotlin.time.Duration.Companion.seconds
* @param onChooseTracks callback to pick a track for the given type of the item * @param onChooseTracks callback to pick a track for the given type of the item
*/ */
fun buildMoreDialogItems( fun buildMoreDialogItems(
context: Context,
item: BaseItem, item: BaseItem,
series: BaseItem?, series: BaseItem?,
sourceId: UUID?, sourceId: UUID?,
@ -50,7 +52,7 @@ fun buildMoreDialogItems(
buildList { buildList {
add( add(
DialogItem( DialogItem(
"Play", context.getString(R.string.play),
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f), iconColor = Color.Green.copy(alpha = .8f),
) { ) {
@ -83,7 +85,7 @@ fun buildMoreDialogItems(
series?.let { series?.let {
add( add(
DialogItem( DialogItem(
"Go to series", context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward, Icons.AutoMirrored.Filled.ArrowForward,
) { ) {
navigateTo( navigateTo(
@ -100,7 +102,10 @@ fun buildMoreDialogItems(
if (sources.size > 1) { if (sources.size > 1) {
add( add(
DialogItem( DialogItem(
"Choose Version", context.getString(
R.string.choose_stream,
context.getString(R.string.version),
),
R.string.fa_file_video, R.string.fa_file_video,
) { ) {
onChooseVersion.invoke() onChooseVersion.invoke()
@ -116,7 +121,10 @@ fun buildMoreDialogItems(
if (audioCount > 1) { if (audioCount > 1) {
add( add(
DialogItem( DialogItem(
"Choose audio", context.getString(
R.string.choose_stream,
context.getString(R.string.audio),
),
R.string.fa_volume_low, R.string.fa_volume_low,
) { ) {
onChooseTracks.invoke(MediaStreamType.AUDIO) onChooseTracks.invoke(MediaStreamType.AUDIO)
@ -126,7 +134,10 @@ fun buildMoreDialogItems(
if (subtitleCount > 0) { if (subtitleCount > 0) {
add( add(
DialogItem( DialogItem(
"Choose subtitles", context.getString(
R.string.choose_stream,
context.getString(R.string.subtitles),
),
R.string.fa_closed_captioning, R.string.fa_closed_captioning,
) { ) {
onChooseTracks.invoke(MediaStreamType.SUBTITLE) onChooseTracks.invoke(MediaStreamType.SUBTITLE)
@ -138,6 +149,7 @@ fun buildMoreDialogItems(
} }
fun buildMoreDialogItemsForHome( fun buildMoreDialogItemsForHome(
context: Context,
item: BaseItem, item: BaseItem,
seriesId: UUID?, seriesId: UUID?,
playbackPosition: Duration, playbackPosition: Duration,
@ -151,7 +163,7 @@ fun buildMoreDialogItemsForHome(
val itemId = item.id val itemId = item.id
add( add(
DialogItem( DialogItem(
"Go To", context.getString(R.string.go_to),
Icons.Default.ArrowForward, Icons.Default.ArrowForward,
) { ) {
navigateTo(item.destination()) navigateTo(item.destination())
@ -161,7 +173,7 @@ fun buildMoreDialogItemsForHome(
if (playbackPosition >= 1.seconds) { if (playbackPosition >= 1.seconds) {
add( add(
DialogItem( DialogItem(
"Resume", context.getString(R.string.resume),
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f), iconColor = Color.Green.copy(alpha = .8f),
) { ) {
@ -175,7 +187,7 @@ fun buildMoreDialogItemsForHome(
) )
add( add(
DialogItem( DialogItem(
"Restart", context.getString(R.string.restart),
Icons.Default.Refresh, Icons.Default.Refresh,
// iconColor = Color.Green.copy(alpha = .8f), // iconColor = Color.Green.copy(alpha = .8f),
) { ) {
@ -190,7 +202,7 @@ fun buildMoreDialogItemsForHome(
} else { } else {
add( add(
DialogItem( DialogItem(
"Play", context.getString(R.string.play),
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f), iconColor = Color.Green.copy(alpha = .8f),
) { ) {
@ -224,7 +236,7 @@ fun buildMoreDialogItemsForHome(
seriesId?.let { seriesId?.let {
add( add(
DialogItem( DialogItem(
"Go to series", context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward, Icons.AutoMirrored.Filled.ArrowForward,
) { ) {
navigateTo( navigateTo(

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -55,7 +57,12 @@ fun FavoritesPage(
) )
} }
val tabs = listOf("Movies", "TV Shows", "Episodes") val tabs =
listOf(
stringResource(R.string.movies),
stringResource(R.string.tv_shows),
stringResource(R.string.episodes),
)
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }

View file

@ -24,6 +24,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -33,6 +34,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
@ -258,7 +260,7 @@ fun PersonPageContent(
} }
item { item {
LoadingRow( LoadingRow(
title = rowTitle("Movies", movies), title = rowTitle(stringResource(R.string.movies), movies),
state = movies, state = movies,
rowIndex = MOVIE_ROW, rowIndex = MOVIE_ROW,
position = position, position = position,
@ -271,7 +273,7 @@ fun PersonPageContent(
} }
item { item {
LoadingRow( LoadingRow(
title = rowTitle("Series", series), title = rowTitle(stringResource(R.string.tv_shows), series),
state = series, state = series,
rowIndex = SERIES_ROW, rowIndex = SERIES_ROW,
position = position, position = position,
@ -284,7 +286,7 @@ fun PersonPageContent(
} }
item { item {
LoadingRow( LoadingRow(
title = rowTitle("Episodes", episodes), title = rowTitle(stringResource(R.string.episodes), episodes),
state = episodes, state = episodes,
rowIndex = EPISODE_ROW, rowIndex = EPISODE_ROW,
position = position, position = position,
@ -365,9 +367,14 @@ fun PersonHeader(
val age = if (deathdate == null) birthdate.until(LocalDate.now())?.years else null val age = if (deathdate == null) birthdate.until(LocalDate.now())?.years else null
val text = val text =
if (age != null) { if (age != null) {
"Born: ${formatDate(it)} ($age years old)" stringResource(R.string.born) + ": ${formatDate(it)} (${
stringResource(
R.string.years_old,
age,
)
})"
} else { } else {
"Born: ${formatDate(it)}" stringResource(R.string.born) + ": ${formatDate(it)}"
} }
Text( Text(
text = text, text = text,
@ -379,7 +386,7 @@ fun PersonHeader(
} }
birthPlace?.let { birthPlace?.let {
Text( Text(
text = "Birthplace: $it", text = stringResource(R.string.birthplace) + ": $it",
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
maxLines = 1, maxLines = 1,
@ -390,9 +397,14 @@ fun PersonHeader(
val age = birthdate?.until(it)?.years val age = birthdate?.until(it)?.years
val text = val text =
if (age != null) { if (age != null) {
"Died: ${formatDate(it)} ($age years old)" stringResource(R.string.died) + ": ${formatDate(it)} (${
stringResource(
R.string.years_old,
age,
)
})"
} else { } else {
"Died: ${formatDate(it)}" stringResource(R.string.died) + ": ${formatDate(it)}"
} }
Text( Text(
text = text, text = text,

View file

@ -42,6 +42,8 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -123,6 +125,7 @@ fun PlaylistDetails(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: PlaylistViewModel = hiltViewModel(), viewModel: PlaylistViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init(destination.itemId) viewModel.init(destination.itemId)
} }
@ -170,13 +173,13 @@ fun PlaylistDetails(
items = items =
listOf( listOf(
DialogItem( DialogItem(
"Go to", context.getString(R.string.go_to),
Icons.Default.ArrowForward, Icons.Default.ArrowForward,
) { ) {
viewModel.navigationManager.navigateTo(item.destination()) viewModel.navigationManager.navigateTo(item.destination())
}, },
DialogItem( DialogItem(
"Play from here", context.getString(R.string.play_from_here),
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
) { ) {
viewModel.navigationManager.navigateTo( viewModel.navigationManager.navigateTo(
@ -282,7 +285,7 @@ fun PlaylistDetailsContent(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
) { ) {
Text( Text(
text = playlist.name ?: "Playlist", text = playlist.name ?: stringResource(R.string.playlist),
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.displayMedium, style = MaterialTheme.typography.displayMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -31,6 +32,7 @@ import androidx.tv.material3.ListItemDefaults
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -202,7 +204,7 @@ fun DvrScheduleContent(
if (activeRecordings.isEmpty() && scheduledRecordings.isEmpty()) { if (activeRecordings.isEmpty() && scheduledRecordings.isEmpty()) {
item { item {
Text( Text(
text = "No scheduled recordings", text = stringResource(R.string.no_scheduled_recordings),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -213,7 +215,7 @@ fun DvrScheduleContent(
if (activeRecordings.isNotEmpty()) { if (activeRecordings.isNotEmpty()) {
item { item {
Text( Text(
text = "Active", text = stringResource(R.string.active_recordings),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View file

@ -1,9 +1,11 @@
package com.github.damontecres.wholphin.ui.detail.livetv package com.github.damontecres.wholphin.ui.detail.livetv
import android.content.Context
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
@ -16,6 +18,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -45,6 +48,7 @@ const val MAX_HOURS = 48L
class LiveTvViewModel class LiveTvViewModel
@Inject @Inject
constructor( constructor(
@param:ApplicationContext private val context: Context,
val api: ApiClient, val api: ApiClient,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
) : ViewModel() { ) : ViewModel() {
@ -169,7 +173,7 @@ class LiveTvViewModel
startHours = it.toFloat(), startHours = it.toFloat(),
endHours = (it + 1).toFloat(), endHours = (it + 1).toFloat(),
duration = 60.seconds, duration = 60.seconds,
name = "No data", name = context.getString(R.string.no_data),
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
isRecording = false, isRecording = false,

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@ -30,6 +31,7 @@ import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -165,7 +167,14 @@ fun ProgramDialog(
) )
} }
Text( Text(
text = if (isSeriesRecording) "Cancel Series Recording" else "Record Series", text =
if (isSeriesRecording) {
stringResource(
R.string.cancel_series_recording,
)
} else {
stringResource(R.string.record_series)
},
) )
} }
} }
@ -193,7 +202,16 @@ fun ProgramDialog(
) )
} }
Text( Text(
text = if (isRecording) "Cancel Recording" else "Record Program", text =
if (isRecording) {
stringResource(
R.string.cancel_recording,
)
} else {
stringResource(
R.string.record_program,
)
},
) )
} }
} }
@ -210,11 +228,11 @@ fun ProgramDialog(
) { ) {
Icon( Icon(
imageVector = Icons.Default.PlayArrow, imageVector = Icons.Default.PlayArrow,
contentDescription = "Delete", contentDescription = stringResource(R.string.delete),
tint = Color.Green.copy(alpha = .75f), tint = Color.Green.copy(alpha = .75f),
) )
Text( Text(
text = "Watch live", text = stringResource(R.string.watch_live),
) )
} }
} }

View file

@ -150,7 +150,7 @@ fun MovieDetails(
overviewOnClick = { overviewOnClick = {
overviewDialog = overviewDialog =
ItemDetailsDialogInfo( ItemDetailsDialogInfo(
title = movie.name ?: "Unknown", title = movie.name ?: context.getString(R.string.unknown),
overview = movie.data.overview, overview = movie.data.overview,
files = movie.data.mediaSources.orEmpty(), files = movie.data.mediaSources.orEmpty(),
) )
@ -162,6 +162,7 @@ fun MovieDetails(
title = movie.name + " (${movie.data.productionYear ?: ""})", title = movie.name + " (${movie.data.productionYear ?: ""})",
items = items =
buildMoreDialogItems( buildMoreDialogItems(
context = context,
item = movie, item = movie,
watched = movie.data.userData?.played ?: false, watched = movie.data.userData?.played ?: false,
favorite = movie.data.userData?.isFavorite ?: false, favorite = movie.data.userData?.isFavorite ?: false,
@ -172,7 +173,10 @@ fun MovieDetails(
onClickFavorite = viewModel::setFavorite, onClickFavorite = viewModel::setFavorite,
onChooseVersion = { onChooseVersion = {
chooseVersion = chooseVersion =
chooseVersionParams(movie.data.mediaSources!!) { idx -> chooseVersionParams(
context,
movie.data.mediaSources!!,
) { idx ->
val source = movie.data.mediaSources!![idx] val source = movie.data.mediaSources!![idx]
viewModel.savePlayVersion( viewModel.savePlayVersion(
movie, movie,
@ -188,6 +192,7 @@ fun MovieDetails(
)?.let { source -> )?.let { source ->
chooseVersion = chooseVersion =
chooseStream( chooseStream(
context = context,
streams = source.mediaStreams.orEmpty(), streams = source.mediaStreams.orEmpty(),
type = type, type = type,
onClick = { trackIndex -> onClick = { trackIndex ->

View file

@ -147,7 +147,7 @@ fun MovieDetailsHeader(
?.joinToString(", ") { it.name!! } ?.joinToString(", ") { it.name!! }
?.let { ?.let {
Text( Text(
text = "Directed by $it", text = stringResource(R.string.directed_by, it),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@ -183,7 +183,7 @@ fun MovieDetailsHeader(
?.let { ?.let {
if (it.isNotNullOrBlank()) { if (it.isNotNullOrBlank()) {
TitleValueText( TitleValueText(
"Subtitles", stringResource(R.string.subtitles),
it, it,
modifier = Modifier.widthIn(max = 200.dp), modifier = Modifier.widthIn(max = 200.dp),
) )

View file

@ -1,7 +1,9 @@
package com.github.damontecres.wholphin.ui.detail.movie package com.github.damontecres.wholphin.ui.detail.movie
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
@ -25,6 +27,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.ThemeSongPlayer import com.github.damontecres.wholphin.util.ThemeSongPlayer
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -43,6 +46,7 @@ class MovieViewModel
@Inject @Inject
constructor( constructor(
api: ApiClient, api: ApiClient,
@param:ApplicationContext private val context: Context,
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository, val itemPlaybackRepository: ItemPlaybackRepository,
@ -79,7 +83,7 @@ class MovieViewModel
// TODO would be nice to clean up the trailer name // TODO would be nice to clean up the trailer name
// ?.replace(item.name ?: "", "") // ?.replace(item.name ?: "", "")
// ?.removePrefix(" - ") // ?.removePrefix(" - ")
?: "Trailer" ?: context.getString(R.string.trailer)
RemoteTrailer(name, url) RemoteTrailer(name, url)
} }
}.orEmpty() }.orEmpty()

View file

@ -82,7 +82,7 @@ fun FocusedEpisodeFooter(
?.let { ?.let {
if (it.isNotNullOrBlank()) { if (it.isNotNullOrBlank()) {
TitleValueText( TitleValueText(
"Subtitles", stringResource(R.string.subtitles),
it, it,
modifier = Modifier.widthIn(max = 160.dp), modifier = Modifier.widthIn(max = 160.dp),
) )

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.detail.series package com.github.damontecres.wholphin.ui.detail.series
import android.content.Context
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -33,6 +34,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -83,6 +85,7 @@ fun SeriesDetails(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel(), viewModel: SeriesViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init(preferences, destination.itemId, destination.item, null) viewModel.init(preferences, destination.itemId, destination.item, null)
} }
@ -136,7 +139,8 @@ fun SeriesDetails(
onLongClickItem = { season -> onLongClickItem = { season ->
seasonDialog = seasonDialog =
buildDialogForSeason( buildDialogForSeason(
season, context = context,
s = season,
onClickItem = { viewModel.navigateTo(it.destination()) }, onClickItem = { viewModel.navigateTo(it.destination()) },
markPlayed = { played -> markPlayed = { played ->
viewModel.setSeasonWatched(season.id, played) viewModel.setSeasonWatched(season.id, played)
@ -146,7 +150,7 @@ fun SeriesDetails(
overviewOnClick = { overviewOnClick = {
overviewDialog = overviewDialog =
ItemDetailsDialogInfo( ItemDetailsDialogInfo(
title = item.name ?: "Unknown", title = item.name ?: context.getString(R.string.unknown),
overview = item.data.overview, overview = item.data.overview,
files = listOf(), files = listOf(),
) )
@ -161,7 +165,8 @@ fun SeriesDetails(
if (showWatchConfirmation) { if (showWatchConfirmation) {
ConfirmDialog( ConfirmDialog(
title = item.name ?: "", title = item.name ?: "",
body = if (played) "Mark entire series as unplayed?" else "Mark entire series as played?", body =
stringResource(if (played) R.string.mark_entire_series_as_unplayed else R.string.mark_entire_series_as_played),
onCancel = { onCancel = {
showWatchConfirmation = false showWatchConfirmation = false
}, },
@ -343,7 +348,7 @@ fun SeriesDetailsContent(
} }
item { item {
ItemRow( ItemRow(
title = "Seasons", title = stringResource(R.string.tv_seasons),
items = seasons, items = seasons,
onClickItem = { onClickItem = {
position = SEASONS_ROW position = SEASONS_ROW
@ -444,7 +449,7 @@ fun SeriesDetailsHeader(
modifier = modifier, modifier = modifier,
) { ) {
Text( Text(
text = series.name ?: "Unknown", text = series.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@ -487,6 +492,7 @@ fun SeriesDetailsHeader(
} }
fun buildDialogForSeason( fun buildDialogForSeason(
context: Context,
s: BaseItem, s: BaseItem,
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
markPlayed: (Boolean) -> Unit, markPlayed: (Boolean) -> Unit,
@ -494,26 +500,26 @@ fun buildDialogForSeason(
val items = val items =
buildList { buildList {
add( add(
DialogItem("Go to", Icons.Default.PlayArrow) { DialogItem(context.getString(R.string.go_to), Icons.Default.PlayArrow) {
onClickItem.invoke(s) onClickItem.invoke(s)
}, },
) )
if (s.data.userData?.played == true) { if (s.data.userData?.played == true) {
add( add(
DialogItem("Mark as unplayed", R.string.fa_eye) { DialogItem(context.getString(R.string.mark_unwatched), R.string.fa_eye) {
markPlayed.invoke(false) markPlayed.invoke(false)
}, },
) )
} else { } else {
add( add(
DialogItem("Mark as played", R.string.fa_eye_slash) { DialogItem(context.getString(R.string.mark_watched), R.string.fa_eye_slash) {
markPlayed.invoke(true) markPlayed.invoke(true)
}, },
) )
} }
} }
return DialogParams( return DialogParams(
title = s.name ?: "Season", title = s.name ?: context.getString(R.string.tv_season),
fromLongClick = true, fromLongClick = true,
items = items, items = items,
) )

View file

@ -13,8 +13,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.LifecycleStartEffect
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.data.model.chooseSource
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -72,6 +74,7 @@ fun SeriesOverview(
viewModel: SeriesViewModel = hiltViewModel(), viewModel: SeriesViewModel = hiltViewModel(),
initialSeasonEpisode: SeasonEpisodeIds? = null, initialSeasonEpisode: SeasonEpisodeIds? = null,
) { ) {
val context = LocalContext.current
val firstItemFocusRequester = remember { FocusRequester() } val firstItemFocusRequester = remember { FocusRequester() }
val episodeRowFocusRequester = remember { FocusRequester() } val episodeRowFocusRequester = remember { FocusRequester() }
@ -178,7 +181,8 @@ fun SeriesOverview(
title = series.name + " - " + ep.data.seasonEpisode, title = series.name + " - " + ep.data.seasonEpisode,
items = items =
buildMoreDialogItems( buildMoreDialogItems(
ep, context = context,
item = ep,
watched = ep.data.userData?.played ?: false, watched = ep.data.userData?.played ?: false,
favorite = ep.data.userData?.isFavorite ?: false, favorite = ep.data.userData?.isFavorite ?: false,
series = series, series = series,
@ -208,7 +212,10 @@ fun SeriesOverview(
}, },
onChooseVersion = { onChooseVersion = {
chooseVersion = chooseVersion =
chooseVersionParams(ep.data.mediaSources!!) { idx -> chooseVersionParams(
context,
ep.data.mediaSources!!,
) { idx ->
val source = ep.data.mediaSources!![idx] val source = ep.data.mediaSources!![idx]
viewModel.savePlayVersion( viewModel.savePlayVersion(
ep, ep,
@ -224,6 +231,7 @@ fun SeriesOverview(
)?.let { source -> )?.let { source ->
chooseVersion = chooseVersion =
chooseStream( chooseStream(
context = context,
streams = source.mediaStreams.orEmpty(), streams = source.mediaStreams.orEmpty(),
type = type, type = type,
onClick = { trackIndex -> onClick = { trackIndex ->
@ -313,7 +321,7 @@ fun SeriesOverview(
episodeList?.getOrNull(position.episodeRowIndex)?.let { episodeList?.getOrNull(position.episodeRowIndex)?.let {
overviewDialog = overviewDialog =
ItemDetailsDialogInfo( ItemDetailsDialogInfo(
title = it.name ?: "Unknown", title = it.name ?: context.getString(R.string.unknown),
overview = it.data.overview, overview = it.data.overview,
files = it.data.mediaSources.orEmpty(), files = it.data.mediaSources.orEmpty(),
) )

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -42,6 +43,7 @@ import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.data.model.aspectRatioFloat
@ -178,7 +180,9 @@ fun SeriesOverviewContent(
.focusRequester(focusRequesters[index]), .focusRequester(focusRequesters[index]),
) { ) {
Text( Text(
text = season.name ?: "Season ${season.data.indexNumber}", text =
season.name
?: (stringResource(R.string.tv_season) + " ${season.data.indexNumber}"),
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
) )
} }

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.ui.main package com.github.damontecres.wholphin.ui.main
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -71,10 +72,20 @@ import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration import kotlin.time.Duration
data class HomeRow( data class HomeRow(
val section: HomeSection, @param:StringRes val titleRes: Int?,
val title: String?,
val items: List<BaseItem?>, val items: List<BaseItem?>,
val title: String? = null, ) {
) constructor(
@StringRes titleRes: Int,
items: List<BaseItem?>,
) : this(titleRes, null, items)
constructor(
title: String,
items: List<BaseItem?>,
) : this(null, title, items)
}
@Composable @Composable
fun HomePage( fun HomePage(
@ -121,6 +132,7 @@ fun HomePage(
onLongClickItem = { onLongClickItem = {
val dialogItems = val dialogItems =
buildMoreDialogItemsForHome( buildMoreDialogItemsForHome(
context = context,
item = it, item = it,
seriesId = it.data.seriesId, seriesId = it.data.seriesId,
playbackPosition = playbackPosition =
@ -242,7 +254,7 @@ fun HomePageContent(
itemsIndexed(homeRows) { rowIndex, row -> itemsIndexed(homeRows) { rowIndex, row ->
if (row.items.isNotEmpty()) { if (row.items.isNotEmpty()) {
ItemRow( ItemRow(
title = row.title ?: stringResource(row.section.nameRes), title = row.title ?: row.titleRes?.let { stringResource(it) } ?: "",
items = row.items, items = row.items,
onClickItem = onClickItem, onClickItem = onClickItem,
cardOnFocus = { isFocused, index -> cardOnFocus = { isFocused, index ->

View file

@ -1,30 +0,0 @@
package com.github.damontecres.wholphin.ui.main
import androidx.annotation.StringRes
import com.github.damontecres.wholphin.R
/**
* All possible homesections, "synced" with jellyfin-web.
*
* https://github.com/jellyfin/jellyfin-web/blob/master/src/components/homesections/homesections.js
*/
enum class HomeSection(
val key: String,
@param:StringRes val nameRes: Int,
) {
LATEST_MEDIA("latestmedia", R.string.home_section_latest_media),
LIBRARY_TILES_SMALL("smalllibrarytiles", R.string.home_section_library),
LIBRARY_BUTTONS("librarybuttons", R.string.home_section_library_small),
RESUME("resume", R.string.home_section_resume),
RESUME_AUDIO("resumeaudio", R.string.home_section_resume_audio),
RESUME_BOOK("resumebook", R.string.home_section_resume_book),
ACTIVE_RECORDINGS("activerecordings", R.string.home_section_active_recordings),
NEXT_UP("nextup", R.string.home_section_next_up),
LIVE_TV("livetv", R.string.home_section_livetv),
NONE("none", R.string.home_section_none),
;
companion object {
fun fromKey(key: String): HomeSection = entries.firstOrNull { it.key == key } ?: NONE
}
}

View file

@ -1,8 +1,10 @@
package com.github.damontecres.wholphin.ui.main package com.github.damontecres.wholphin.ui.main
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.NavDrawerItemRepository import com.github.damontecres.wholphin.data.NavDrawerItemRepository
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
@ -15,6 +17,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.supportItemKinds import com.github.damontecres.wholphin.util.supportItemKinds
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -40,6 +43,7 @@ import javax.inject.Inject
class HomeViewModel class HomeViewModel
@Inject @Inject
constructor( constructor(
@param:ApplicationContext private val context: Context,
val api: ApiClient, val api: ApiClient,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
@ -86,7 +90,7 @@ class HomeViewModel
if (resume.isNotEmpty()) { if (resume.isNotEmpty()) {
add( add(
HomeRow( HomeRow(
section = HomeSection.RESUME, titleRes = R.string.recently_added,
items = resume, items = resume,
), ),
) )
@ -94,7 +98,7 @@ class HomeViewModel
if (nextUp.isNotEmpty()) { if (nextUp.isNotEmpty()) {
add( add(
HomeRow( HomeRow(
section = HomeSection.NEXT_UP, titleRes = R.string.next_up,
items = nextUp, items = nextUp,
), ),
) )
@ -191,10 +195,10 @@ class HomeViewModel
.mapNotNull { view -> .mapNotNull { view ->
val title = val title =
if (view.collectionType == CollectionType.LIVETV) { if (view.collectionType == CollectionType.LIVETV) {
"Recently Recorded" context.getString(R.string.recently_recorded)
} else { } else {
view.name?.let { "Recently Added in $it" } view.name?.let { context.getString(R.string.recently_added_in, it) }
} } ?: context.getString(R.string.recently_added)
val viewId = val viewId =
if (view.collectionType == CollectionType.LIVETV) { if (view.collectionType == CollectionType.LIVETV) {
api.liveTvApi api.liveTvApi
@ -222,9 +226,8 @@ class HomeViewModel
.content .content
.map { BaseItem.from(it, api, true) } .map { BaseItem.from(it, api, true) }
HomeRow( HomeRow(
section = HomeSection.LATEST_MEDIA,
items = latest,
title = title, title = title,
items = latest,
) )
} }
} }

View file

@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -28,6 +30,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
@ -153,6 +156,7 @@ fun SearchPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SearchViewModel = hiltViewModel(), viewModel: SearchViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current
val movies by viewModel.movies.observeAsState(SearchResult.NoQuery) val movies by viewModel.movies.observeAsState(SearchResult.NoQuery)
val collections by viewModel.collections.observeAsState(SearchResult.NoQuery) val collections by viewModel.collections.observeAsState(SearchResult.NoQuery)
val series by viewModel.series.observeAsState(SearchResult.NoQuery) val series by viewModel.series.observeAsState(SearchResult.NoQuery)
@ -198,7 +202,7 @@ fun SearchPage(
} }
} }
searchResultRow( searchResultRow(
title = "Movies", title = context.getString(R.string.movies),
result = movies, result = movies,
rowIndex = MOVIE_ROW, rowIndex = MOVIE_ROW,
position = position, position = position,
@ -208,7 +212,7 @@ fun SearchPage(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
searchResultRow( searchResultRow(
title = "Collections", title = context.getString(R.string.collections),
result = collections, result = collections,
rowIndex = COLLECTION_ROW, rowIndex = COLLECTION_ROW,
position = position, position = position,
@ -218,7 +222,7 @@ fun SearchPage(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
searchResultRow( searchResultRow(
title = "Series", title = context.getString(R.string.tv_shows),
result = series, result = series,
rowIndex = SERIES_ROW, rowIndex = SERIES_ROW,
position = position, position = position,
@ -228,7 +232,7 @@ fun SearchPage(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
searchResultRow( searchResultRow(
title = "Episodes", title = context.getString(R.string.episodes),
result = episodes, result = episodes,
rowIndex = EPISODE_ROW, rowIndex = EPISODE_ROW,
position = position, position = position,
@ -308,7 +312,7 @@ fun LazyListScope.searchResultRow(
SearchResult.Searching -> SearchResult.Searching ->
SearchResultPlaceholder( SearchResultPlaceholder(
title = title, title = title,
message = "Searching...", message = stringResource(R.string.searching),
modifier = modifier, modifier = modifier,
) )
@ -316,7 +320,7 @@ fun LazyListScope.searchResultRow(
if (r.items.isEmpty()) { if (r.items.isEmpty()) {
SearchResultPlaceholder( SearchResultPlaceholder(
title = title, title = title,
message = "No results", message = stringResource(R.string.no_results),
modifier = modifier, modifier = modifier,
) )
} else { } else {

View file

@ -324,7 +324,7 @@ fun NavDrawer(
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
LaunchedEffect(focused) { if (focused) focusedIndex = -2 } LaunchedEffect(focused) { if (focused) focusedIndex = -2 }
IconNavItem( IconNavItem(
text = "Search", text = stringResource(R.string.search),
icon = Icons.Default.Search, icon = Icons.Default.Search,
selected = selectedIndex == -2, selected = selectedIndex == -2,
interactionSource = interactionSource, interactionSource = interactionSource,
@ -345,7 +345,7 @@ fun NavDrawer(
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
LaunchedEffect(focused) { if (focused) focusedIndex = -1 } LaunchedEffect(focused) { if (focused) focusedIndex = -1 }
IconNavItem( IconNavItem(
text = "Home", text = stringResource(R.string.home),
icon = Icons.Default.Home, icon = Icons.Default.Home,
selected = selectedIndex == -1, selected = selectedIndex == -1,
interactionSource = interactionSource, interactionSource = interactionSource,
@ -413,7 +413,7 @@ fun NavDrawer(
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE } LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE }
IconNavItem( IconNavItem(
text = "Settings", text = stringResource(R.string.settings),
icon = Icons.Default.Settings, icon = Icons.Default.Settings,
selected = false, selected = false,
interactionSource = interactionSource, interactionSource = interactionSource,

View file

@ -28,6 +28,8 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
@ -35,6 +37,7 @@ import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogItemDivider import com.github.damontecres.wholphin.ui.components.DialogItemDivider
@ -58,7 +61,7 @@ fun DownloadSubtitlesContent(
SubtitleSearch.Searching -> { SubtitleSearch.Searching -> {
Wrapper { Wrapper {
Text( Text(
text = "Searching...", text = stringResource(R.string.searching),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@ -68,7 +71,7 @@ fun DownloadSubtitlesContent(
SubtitleSearch.Downloading -> { SubtitleSearch.Downloading -> {
Wrapper { Wrapper {
Text( Text(
text = "Downloading...", text = stringResource(R.string.downloading),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@ -199,9 +202,15 @@ fun convertRemoteSubtitles(
op.providerName?.let(::add) op.providerName?.let(::add)
op.threeLetterIsoLanguageName?.let(::add) op.threeLetterIsoLanguageName?.let(::add)
if (op.forced == true) { if (op.forced == true) {
add("Forced") add(stringResource(R.string.forced_track))
} }
add("${abbreviateNumber(op.downloadCount ?: 0)} downloads") add(
pluralStringResource(
R.plurals.downloads,
op.downloadCount ?: 0,
abbreviateNumber(op.downloadCount ?: 0),
),
)
} }
Text( Text(
text = strings.joinToString(" - "), text = strings.joinToString(" - "),

View file

@ -1,5 +1,8 @@
package com.github.damontecres.wholphin.ui.playback package com.github.damontecres.wholphin.ui.playback
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.WholphinApplication
data class SubtitleStream( data class SubtitleStream(
val index: Int, val index: Int,
val language: String?, val language: String?,
@ -17,7 +20,8 @@ data class SubtitleStream(
language, language,
title, title,
codec, codec,
).joinToString(" - ").ifBlank { "Unknown" } ).joinToString(" - ")
.ifBlank { WholphinApplication.instance.getString(R.string.unknown) }
} }
data class AudioStream( data class AudioStream(

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -34,6 +35,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@ -68,7 +70,7 @@ fun NextUpEpisode(
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
) { ) {
Text( Text(
text = "Up Next...", text = stringResource(R.string.next_up) + "...",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View file

@ -46,6 +46,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@ -66,6 +67,7 @@ import com.github.damontecres.wholphin.ui.seekBack
import com.github.damontecres.wholphin.ui.seekForward import com.github.damontecres.wholphin.ui.seekForward
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.stringRes
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -212,7 +214,7 @@ fun PlaybackControls(
.padding(end = 32.dp), .padding(end = 32.dp),
) { ) {
Text( Text(
text = "Skip ${segment.type.serialName}", text = stringResource(R.string.skip) + " " + stringResource(segment.type.stringRes),
) )
} }
} }
@ -332,7 +334,7 @@ fun LeftPlaybackButtons(
val options = val options =
buildList { buildList {
addAll(moreButtonOptions.options.keys) addAll(moreButtonOptions.options.keys)
add(if (showDebugInfo) "Hide debug info" else "Show debug info") add(stringResource(if (showDebugInfo) R.string.hide_debug_info else R.string.show_debug_info))
} }
BottomDialog( BottomDialog(
choices = options, choices = options,
@ -407,7 +409,12 @@ fun RightPlaybackButtons(
val currentChoice = val currentChoice =
subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex } ?: subtitleStreams.size subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex } ?: subtitleStreams.size
BottomDialog( BottomDialog(
choices = options + listOf("None", "Search & Download"), choices =
options +
listOf(
stringResource(R.string.none),
stringResource(R.string.search_and_download),
),
currentChoice = currentChoice, currentChoice = currentChoice,
onDismissRequest = { onDismissRequest = {
onControllerInteraction.invoke() onControllerInteraction.invoke()
@ -434,7 +441,12 @@ fun RightPlaybackButtons(
) )
} }
if (showOptionsDialog) { if (showOptionsDialog) {
val options = listOf("Audio Track", "Playback Speed", "Video Scale") val options =
listOf(
stringResource(R.string.audio),
stringResource(R.string.playback_speed),
stringResource(R.string.video_scale),
)
BottomDialog( BottomDialog(
choices = options, choices = options,
currentChoice = null, currentChoice = null,

View file

@ -38,11 +38,13 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.Chapter
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
@ -199,7 +201,7 @@ fun PlaybackOverlay(
}, },
) { ) {
Text( Text(
text = "Chapters", text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
) )
LazyRow( LazyRow(
@ -240,7 +242,7 @@ fun PlaybackOverlay(
} }
if (playlist.hasNext()) { if (playlist.hasNext()) {
Text( Text(
text = "Queue", text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = modifier =
Modifier Modifier
@ -282,7 +284,7 @@ fun PlaybackOverlay(
}, },
) { ) {
Text( Text(
text = "Queue", text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
) )
LazyRow( LazyRow(
@ -489,7 +491,7 @@ fun Controller(
when (nextState) { when (nextState) {
OverlayViewState.CHAPTERS -> OverlayViewState.CHAPTERS ->
Text( Text(
text = "Chapters", text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = modifier =
Modifier Modifier
@ -501,7 +503,7 @@ fun Controller(
OverlayViewState.QUEUE -> OverlayViewState.QUEUE ->
Text( Text(
text = "Queue", text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = modifier =
Modifier Modifier

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@ -60,6 +61,7 @@ import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.Playlist
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -72,6 +74,7 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.seasonEpisode import com.github.damontecres.wholphin.util.seasonEpisode
import com.github.damontecres.wholphin.util.stringRes
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
@ -411,7 +414,7 @@ fun PlaybackPage(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
) { ) {
Text( Text(
text = "Skip ${segment.type.serialName}", text = stringResource(R.string.skip) + " " + stringResource(segment.type.stringRes),
) )
} }
} }

View file

@ -19,6 +19,7 @@ import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
@ -963,7 +964,7 @@ class PlaybackViewModel
if (maxAttempts == 0) { if (maxAttempts == 0) {
showToast( showToast(
context, context,
"Download is taking a long time, you may need to restart playback", context.getString(R.string.subtitle_download_too_long),
) )
} else { } else {
// Find the new subtitle stream // Find the new subtitle stream

View file

@ -97,9 +97,9 @@ fun PreferencesContent(
} }
val screenTitle = val screenTitle =
when (preferenceScreenOption) { when (preferenceScreenOption) {
PreferenceScreenOption.BASIC -> "Preferences" PreferenceScreenOption.BASIC -> R.string.settings
PreferenceScreenOption.ADVANCED -> "Advanced Preferences" PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
PreferenceScreenOption.USER_INTERFACE -> "User Interface Preferences" PreferenceScreenOption.USER_INTERFACE -> R.string.ui_interface
} }
var visible by remember { mutableStateOf(false) } var visible by remember { mutableStateOf(false) }
@ -126,7 +126,7 @@ fun PreferencesContent(
) { ) {
stickyHeader { stickyHeader {
Text( Text(
text = screenTitle, text = stringResource(screenTitle),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -42,6 +43,7 @@ import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -221,7 +223,7 @@ fun InstallUpdatePageContent(
).padding(16.dp), ).padding(16.dp),
) { ) {
Text( Text(
text = "Update available", text = stringResource(R.string.update_available),
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@ -234,14 +236,14 @@ fun InstallUpdatePageContent(
onClick = onInstallRelease, onClick = onInstallRelease,
) { ) {
Text( Text(
text = "Download & Update", text = stringResource(R.string.download_and_update),
) )
} }
Button( Button(
onClick = onCancel, onClick = onCancel,
) { ) {
Text( Text(
text = "Cancel", text = stringResource(R.string.cancel),
) )
} }
} }

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
@ -96,7 +97,7 @@ fun ServerList(
ListItem( ListItem(
enabled = true, enabled = true,
selected = false, selected = false,
headlineContent = { Text(text = "Add Server") }, headlineContent = { Text(text = stringResource(R.string.add_server)) },
leadingContent = { leadingContent = {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
@ -116,11 +117,14 @@ fun ServerList(
title = server.name ?: server.url, title = server.name ?: server.url,
dialogItems = dialogItems =
listOf( listOf(
DialogItem("Switch", R.string.fa_arrow_left_arrow_right) { DialogItem(
stringResource(R.string.switch_servers),
R.string.fa_arrow_left_arrow_right,
) {
onSwitchServer.invoke(server) onSwitchServer.invoke(server)
}, },
DialogItem( DialogItem(
"Delete", stringResource(R.string.delete),
Icons.Default.Delete, Icons.Default.Delete,
Color.Red.copy(alpha = .8f), Color.Red.copy(alpha = .8f),
) { ) {

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -31,6 +32,7 @@ import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.EditTextBox
@ -78,7 +80,7 @@ fun SwitchServerContent(
), ),
) { ) {
Text( Text(
text = "Select Server", text = stringResource(R.string.select_server),
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@ -120,13 +122,13 @@ fun SwitchServerContent(
), ),
) { ) {
Text( Text(
text = "Discovered Servers", text = stringResource(R.string.discovered_servers),
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
if (discoveredServers.isEmpty()) { if (discoveredServers.isEmpty()) {
Text( Text(
text = "Searching...", text = stringResource(R.string.searching),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@ -184,7 +186,7 @@ fun SwitchServerContent(
.fillMaxWidth(.4f), .fillMaxWidth(.4f),
) { ) {
Text( Text(
text = "Enter Server URL", text = stringResource(R.string.enter_server_url),
) )
EditTextBox( EditTextBox(
value = url, value = url,
@ -227,7 +229,7 @@ fun SwitchServerContent(
if (state == LoadingState.Loading) { if (state == LoadingState.Loading) {
CircularProgress(Modifier.size(32.dp)) CircularProgress(Modifier.size(32.dp))
} else { } else {
Text(text = "Submit") Text(text = stringResource(R.string.submit))
} }
} }
} }

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@ -37,6 +38,7 @@ import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.EditTextBox
@ -94,7 +96,7 @@ fun SwitchUserContent(
), ),
) { ) {
Text( Text(
text = "Select User", text = stringResource(R.string.select_user),
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )

View file

@ -67,7 +67,7 @@ fun UserList(
ListItem( ListItem(
enabled = true, enabled = true,
selected = false, selected = false,
headlineContent = { Text(text = "Add User") }, headlineContent = { Text(text = stringResource(R.string.add_user)) },
leadingContent = { leadingContent = {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
@ -84,7 +84,7 @@ fun UserList(
ListItem( ListItem(
enabled = true, enabled = true,
selected = false, selected = false,
headlineContent = { Text(text = "Switch servers") }, headlineContent = { Text(text = stringResource(R.string.switch_servers)) },
leadingContent = { leadingContent = {
Text( Text(
text = stringResource(R.string.fa_arrow_left_arrow_right), text = stringResource(R.string.fa_arrow_left_arrow_right),
@ -102,11 +102,14 @@ fun UserList(
title = user.name ?: user.id.toString(), title = user.name ?: user.id.toString(),
dialogItems = dialogItems =
listOf( listOf(
DialogItem("Switch", R.string.fa_arrow_left_arrow_right) { DialogItem(
stringResource(R.string.switch_user),
R.string.fa_arrow_left_arrow_right,
) {
onSwitchUser.invoke(user) onSwitchUser.invoke(user)
}, },
DialogItem( DialogItem(
"Delete", stringResource(R.string.delete),
Icons.Default.Delete, Icons.Default.Delete,
Color.Red.copy(alpha = .8f), Color.Red.copy(alpha = .8f),
) { ) {

View file

@ -1,16 +1,8 @@
package com.github.damontecres.wholphin.util package com.github.damontecres.wholphin.util
import com.github.damontecres.wholphin.ui.main.HomeSection
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
val supportedHomeSection =
setOf(
HomeSection.LATEST_MEDIA,
HomeSection.NEXT_UP,
HomeSection.RESUME,
)
val supportItemKinds = val supportItemKinds =
setOf( setOf(
BaseItemKind.MOVIE, BaseItemKind.MOVIE,

View file

@ -48,7 +48,7 @@ class CrashReportSender : ReportSender {
val api = val api =
createJellyfin { createJellyfin {
this.context = context this.context = context
clientInfo = AppModule.clientInfo() clientInfo = AppModule.clientInfo(context)
deviceInfo = AppModule.deviceInfo(context) deviceInfo = AppModule.deviceInfo(context)
apiClientFactory = okHttpFactory apiClientFactory = okHttpFactory
socketConnectionFactory = okHttpFactory socketConnectionFactory = okHttpFactory

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.util package com.github.damontecres.wholphin.util
import android.os.Build import android.os.Build
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
@ -11,6 +12,7 @@ import com.github.damontecres.wholphin.data.model.chooseStream
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.MediaSegmentType
import org.jellyfin.sdk.model.api.MediaStream import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import java.time.LocalDate import java.time.LocalDate
@ -164,3 +166,15 @@ fun formatBytes(
} }
return String.format(Locale.getDefault(), "%.2f%s", count, suffixes[unit]) return String.format(Locale.getDefault(), "%.2f%s", count, suffixes[unit])
} }
@get:StringRes
val MediaSegmentType.stringRes: Int
get() =
when (this) {
MediaSegmentType.UNKNOWN -> R.string.unknown
MediaSegmentType.COMMERCIAL -> R.string.commercial
MediaSegmentType.PREVIEW -> R.string.preview
MediaSegmentType.RECAP -> R.string.recap
MediaSegmentType.OUTRO -> R.string.outro
MediaSegmentType.INTRO -> R.string.intro
}

View file

@ -1,5 +1,53 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="pref_key_update_last_check_threshold" translatable="false">preference.update.threshold</string>
<string name="pref_key_update_last_check" translatable="false">preference.update.lastTimestamp</string>
<string name="ac3_supported">Device supports AC3/Dolby Digital</string>
<string name="advanced_settings">Advanced Settings</string>
<string name="advanced_ui">Advanced UI</string>
<string name="app_theme">Application Theme</string>
<string name="auto_check_for_updates">Automatically check for updates</string>
<string name="auto_play_next_delay">Delay before playing next up</string>
<string name="auto_play_next">Auto play next up</string>
<string name="check_for_updates">Check for updates</string>
<string name="combine_continue_next_summary">Applies to TV Series only</string>
<string name="combine_continue_next"><![CDATA[Combine Continue Watching & Next Up]]></string>
<string name="direct_play_ass">Direct play ASS subtitles</string>
<string name="direct_play_pgs">Direct play PGS subtitles</string>
<string name="downmix_stereo">Always downmix to stereo</string>
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string>
<string name="global_content_scale">Default content scale</string>
<string name="install_update">Install update</string>
<string name="installed_version">Installed version</string>
<string name="max_homepage_items">Max items on home page rows</string>
<string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string>
<string name="nav_drawer_pins">Customize Navigation Drawer Items</string>
<string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string>
<string name="nav_drawer_switch_on_focus">Switch nav drawer pages on focus</string>
<string name="pass_out_protection">Passout Protection</string>
<string name="play_theme_music">Play theme music</string>
<string name="playback_overrides">Playback overrides</string>
<string name="remember_selected_tab">Remember selected tabs</string>
<string name="rewatch_next_up">Enable rewatching in next up</string>
<string name="seek_bar_steps">Seek bar steps</string>
<string name="send_app_logs_summary">Useful for debugging</string>
<string name="send_app_logs">Send app logs to current server</string>
<string name="send_crash_reports_summary">Will try to send to last connected server</string>
<string name="send_crash_reports">Send Crash Reports</string>
<string name="settings">Settings</string>
<string name="skip_back_on_resume_preference">Skip back when resuming playback</string>
<string name="skip_back_preference">Skip back</string>
<string name="skip_commercials_behavior">Skip commercials behavior</string>
<string name="skip_forward_preference">Skip forward</string>
<string name="skip_intro_behavior">Skip intro behavior</string>
<string name="skip_outro_behavior">Skip outro behavior</string>
<string name="skip_previews_behavior">Skip previews behavior</string>
<string name="skip_recap_behavior">Skip recap behavior</string>
<string name="update_available">Update available</string>
<string name="update_url_summary">URL used to check for app updates</string>
<string name="update_url">Update URL</string>
<string-array name="theme_song_volume"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>
<item>Lowest</item> <item>Lowest</item>

View file

@ -1,109 +1,157 @@
<resources> <resources>
<string name="app_name">Wholphin</string> <string name="app_name" translatable="false">Wholphin</string>
<string name="app_name_long" translatable="false">Wholphin</string>
<string name="home_section_latest_media">Recently added media</string> <string name="about">About</string>
<string name="home_section_library">My media</string> <string name="active_recordings">Active Recordings</string>
<string name="home_section_library_small">My media (small)</string> <string name="add_favorite">Favorite</string>
<string name="home_section_resume">Continue watching</string> <string name="add_server">Add Server</string>
<string name="home_section_resume_audio">Continue listening</string> <string name="add_user">Add User</string>
<string name="home_section_resume_book">Continue reading</string> <string name="audio">Audio</string>
<string name="home_section_active_recordings">Active recordings</string> <string name="birthplace">Birthplace</string>
<string name="home_section_next_up">Next up</string> <string name="bitrate">Bitrate</string>
<string name="home_section_livetv">Live TV</string> <string name="born">Born</string>
<string name="home_section_none">None</string> <string name="cancel_recording">Cancel Recording</string>
<string name="search">Search</string> <string name="cancel_series_recording">Cancel Series Recording</string>
<string name="resume">Resume</string> <string name="cancel">Cancel</string>
<string name="restart">Restart</string> <string name="chapters">Chapters</string>
<string name="play">Play</string> <string name="choose_stream">Choose %1$s</string>
<string name="more">More</string> <string name="clear_image_cache">Clear image cache</string>
<string name="collection">Collection</string>
<string name="collections">Collections</string>
<string name="commercial">Commercial</string>
<string name="community_rating">Community Rating</string>
<string name="compose_error_message_title">An error occurred! Press the button to send logs to your server.</string>
<string name="confirm">Confirm</string>
<string name="continue_watching">Continue watching</string>
<string name="critic_rating">Critic Rating</string>
<string name="decimal_seconds">%.1f seconds</string>
<string name="default_track">Default</string>
<string name="delete">Delete</string>
<string name="died">Died</string>
<string name="directed_by">Directed by %1$s</string>
<string name="director">Director</string>
<string name="disabled">Disabled</string>
<string name="discovered_servers">Discovered Servers</string>
<string name="download_and_update"><![CDATA[Download & Update]]></string>
<string name="downloading">Downloading…</string>
<string name="enabled">Enabled</string>
<string name="enter_server_url">Enter Server IP or URL including port</string>
<string name="episodes">Episodes</string>
<string name="error_loading_collection">Error loading collection %1$s</string>
<string name="external_track">External</string>
<string name="favorites">Favorites</string>
<string name="file_size">Size</string>
<string name="forced_track">Forced</string>
<string name="genres">Genres</string>
<string name="go_to_series">Go to series</string>
<string name="go_to">Go To</string>
<string name="hide_controller_timeout">Hide playback controls</string>
<string name="hide_debug_info">Hide debug info</string>
<string name="hide">Hide</string>
<string name="home">Home</string>
<string name="immediate">Immediate</string>
<string name="intro">Intro</string>
<string name="jump_letters">#ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
<string name="library">Library</string>
<string name="license_info">License information</string>
<string name="live_tv">Live TV</string>
<string name="loading">Loading…</string>
<string name="mark_entire_series_as_played">Mark entire series as played?</string>
<string name="mark_entire_series_as_unplayed">Mark entire series as unplayed?</string>
<string name="mark_unwatched">Mark as unwatched</string> <string name="mark_unwatched">Mark as unwatched</string>
<string name="mark_watched">Mark as watched</string> <string name="mark_watched">Mark as watched</string>
<string name="cancel">Cancel</string> <string name="max_bitrate">Max bitrate</string>
<string name="save">Save</string> <string name="more_like_this">More like this</string>
<string name="app_name_long">Wholphin</string> <string name="more">More</string>
<string name="ui_interface">Interface</string> <string name="movies">Movies</string>
<string name="playback">Playback</string> <string name="name">Name</string>
<string name="about">About</string> <string name="next_up">Next Up</string>
<string name="advanced_settings">Advanced Settings</string> <string name="no_data">No data</string>
<string name="advanced_ui">Advanced UI</string> <string name="no_results">No results</string>
<string name="install_update">Install update</string> <string name="no_scheduled_recordings">No scheduled recordings</string>
<string name="installed_version">Installed version</string> <string name="no_update_available">No update available</string>
<string name="show">Show</string> <string name="none">None</string>
<string name="hide">Hide</string> <string name="outro">Outro</string>
<string name="enabled">Enabled</string> <string name="path">Path</string>
<string name="disabled">Disabled</string> <string name="people">People</string>
<string name="skip_forward_preference">Skip forward</string> <string name="play_count">Play Count</string>
<string name="skip_back_preference">Skip back</string> <string name="play_from_here">Play from here</string>
<string name="hide_controller_timeout">Hide playback controls</string> <string name="play">Play</string>
<string name="seek_bar_steps">Seek bar steps</string>
<string name="max_homepage_items">Max items on home page rows</string>
<string name="rewatch_next_up">Enable rewatching in next up</string>
<string name="studios">Studios</string>
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="genres">Genres</string>
<string name="play_theme_music">Play theme music</string>
<string name="license_info">License information</string>
<string name="playback_debug_info">Show playback debug info</string> <string name="playback_debug_info">Show playback debug info</string>
<string name="auto_play_next_delay">Delay before playing next up</string> <string name="playback_speed">Playback Speed</string>
<string name="auto_play_next">Auto play next up</string> <string name="playback">Playback</string>
<string name="playlist">Playlist</string>
<string name="preview">Preview</string>
<string name="sort_by_name">Name</string> <string name="profile_specific_settings">User Profile Settings</string>
<string name="sort_by_random">Random</string> <string name="queue">Queue</string>
<string name="sort_by_date_episode_added">Date Episode Added</string> <string name="recap">Recap</string>
<string name="recently_added_in">Recently added in %1$s</string>
<string name="recently_added">Recently added</string>
<string name="recently_recorded">Recently Recorded</string>
<string name="recently_released">Recently Released</string>
<string name="recommended">Recommended</string>
<string name="record_program">Record Program</string>
<string name="record_series">Record Series</string>
<string name="remove_favorite">Unfavorite</string>
<string name="restart">Restart</string>
<string name="resume">Resume</string>
<string name="save">Save</string>
<string name="search_and_download"><![CDATA[Search & Download]]></string>
<string name="search">Search</string>
<string name="searching">Searching…</string>
<string name="select_server">Select Server</string>
<string name="select_user">Select User</string>
<string name="show_debug_info">Show debug info</string>
<string name="show_next_up_when">Show next up</string>
<string name="show">Show</string>
<string name="shuffle">Shuffle</string>
<string name="skip">Skip</string>
<string name="sort_by_date_added">Date Added</string> <string name="sort_by_date_added">Date Added</string>
<string name="sort_by_date_episode_added">Date Episode Added</string>
<string name="sort_by_date_played">Date Played</string> <string name="sort_by_date_played">Date Played</string>
<string name="sort_by_date_released">Date Released</string> <string name="sort_by_date_released">Date Released</string>
<string name="skip_back_on_resume_preference">Skip back when resuming playback</string> <string name="sort_by_name">Name</string>
<string name="pref_key_update_last_check_threshold">preference.update.threshold</string> <string name="sort_by_random">Random</string>
<string name="pref_key_update_last_check">preference.update.lastTimestamp</string> <string name="studios">Studios</string>
<string name="confirm">Confirm</string> <string name="submit">Submit</string>
<string name="remember_selected_tab">Remember selected tabs</string> <string name="subtitle_download_too_long">Download is taking a long time, you may need to restart playback</string>
<string name="director">Director</string> <string name="subtitle">Subtitle</string>
<string name="max_bitrate">Max bitrate</string> <string name="subtitles">Subtitles</string>
<string name="app_theme">Application Theme</string> <string name="suggestions">Suggestions</string>
<string name="skip_recap_behavior">Skip recap behavior</string> <string name="switch_servers">Switch servers</string>
<string name="skip_previews_behavior">Skip previews behavior</string> <string name="switch_user">Switch</string>
<string name="skip_comercials_behavior">Skip commercials behavior</string> <string name="top_unwatched">Top Rated Unwatched</string>
<string name="skip_outro_behavior">Skip outro behavior</string> <string name="trailer">Trailer</string>
<string name="skip_intro_behavior">Skip intro behavior</string>
<string name="check_for_updates">Check for updates</string>
<string name="auto_check_for_updates">Automatically check for updates</string>
<string name="update_url">Update URL</string>
<string name="update_url_summary">URL used to check for app updates</string>
<string name="updates">Updates</string>
<string name="no_update_available">No update available</string>
<string name="clear_image_cache">Clear image cache</string>
<string name="shuffle">Shuffle</string>
<string name="combine_continue_next"><![CDATA[Combine Continue Watching & Next Up]]></string>
<string name="ac3_supported">Device supports AC3/Dolby Digital</string>
<string name="downmix_stereo">Always downmix to stereo</string>
<string name="playback_overrides">Playback overrides</string>
<string name="direct_play_ass">Direct play ASS subtitles</string>
<string name="direct_play_pgs">Direct play PGS subtitles</string>
<string name="trailers">Trailers</string> <string name="trailers">Trailers</string>
<string name="pass_out_protection">Passout Protection</string> <string name="tv_dvr_schedule">DVR Schedule</string>
<string name="more_like_this">More like this</string> <string name="tv_guide">Guide</string>
<string name="remove_favorite">Unfavorite</string> <string name="tv_season">Season</string>
<string name="add_favorite">Favorite</string> <string name="tv_seasons">Seasons</string>
<string name="favorites">Favorites</string> <string name="tv_shows">TV Shows</string>
<string name="nav_drawer_pins">Customize Navigation Drawer Items</string> <string name="ui_interface">Interface</string>
<string name="profile_specific_settings">User Profile Settings</string> <string name="unknown">Unknown</string>
<string name="nav_drawer_pins_summary">Choose the default items to show, others will be hidden</string> <string name="updates">Updates</string>
<string name="combine_continue_next_summary">Applies to TV Series only</string> <string name="version">Version</string>
<string name="send_crash_reports">Send Crash Reports</string> <string name="video_scale">Video Scale</string>
<string name="send_crash_reports_summary">Will try to send to last connected server</string> <string name="video">Video</string>
<string name="send_app_logs">Send app logs to current server</string> <string name="watch_live">Watch live</string>
<string name="send_app_logs_summary">Useful for debugging</string> <string name="years_old">%1$d years old</string>
<string name="global_content_scale">Default content scale</string>
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string> <plurals name="downloads">
<string name="nav_drawer_switch_on_focus">Switch nav drawer pages on focus</string> <item quantity="zero">%d downloads</item>
<string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string> <item quantity="one">%d download</item>
<string name="show_next_up_when">Show next up</string> <item quantity="other">%d downloads</item>
<string name="next_up">Next Up</string> </plurals>
<string name="community_rating">Community Rating</string> <plurals name="hours">
<string name="critic_rating">Critic Rating</string> <item quantity="zero">%d hours</item>
<string name="play_count">Play Count</string> <item quantity="one">%d hour</item>
<item quantity="other">%d hours</item>
</plurals>
<plurals name="seconds">
<item quantity="zero">%d seconds</item>
<item quantity="one">%d second</item>
<item quantity="other">%d seconds</item>
</plurals>
</resources> </resources>