From 715dc1abda9928265137b648144ed26ec45a8a5a Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Sat, 1 Nov 2025 19:25:19 -0400
Subject: [PATCH 1/8] Fix network on main during letter position lookups (#136)
Maybe fixes #135
Yet another network-on-main exception fix.
---
.../ui/components/CollectionFolderGrid.kt | 40 ++++++++++---------
.../wholphin/ui/components/GenreCardGrid.kt | 23 ++++++-----
.../wholphin/ui/detail/CardGrid.kt | 7 +++-
3 files changed, 39 insertions(+), 31 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
index 0a7e1ff6..8d5139ca 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
@@ -158,26 +158,28 @@ class CollectionFolderViewModel
}
suspend fun positionOfLetter(letter: Char): Int? =
- item.value?.let { item ->
- val includeItemTypes =
- when (item.data.collectionType) {
- CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
- CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
- CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
+ withContext(Dispatchers.IO) {
+ item.value?.let { item ->
+ val includeItemTypes =
+ when (item.data.collectionType) {
+ CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
+ CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
+ CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
- else -> listOf()
- }
- val request =
- GetItemsRequest(
- parentId = item.id,
- includeItemTypes = includeItemTypes,
- nameLessThan = letter.toString(),
- limit = 0,
- enableTotalRecordCount = true,
- recursive = true,
- )
- val result by GetItemsRequestHandler.execute(api, request)
- result.totalRecordCount
+ else -> listOf()
+ }
+ val request =
+ GetItemsRequest(
+ parentId = item.id,
+ includeItemTypes = includeItemTypes,
+ nameLessThan = letter.toString(),
+ limit = 0,
+ enableTotalRecordCount = true,
+ recursive = true,
+ )
+ val result by GetItemsRequestHandler.execute(api, request)
+ result.totalRecordCount
+ }
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt
index 94710301..e2ce1b79 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt
@@ -64,17 +64,18 @@ class GenreViewModel
}
}
- suspend fun positionOfLetter(letter: Char): Int {
- val request =
- GetGenresRequest(
- parentId = itemId,
- nameLessThan = letter.toString(),
- limit = 0,
- enableTotalRecordCount = true,
- )
- val result by GetGenresRequestHandler.execute(api, request)
- return result.totalRecordCount
- }
+ suspend fun positionOfLetter(letter: Char): Int =
+ withContext(Dispatchers.IO) {
+ val request =
+ GetGenresRequest(
+ parentId = itemId,
+ nameLessThan = letter.toString(),
+ limit = 0,
+ enableTotalRecordCount = true,
+ )
+ val result by GetGenresRequestHandler.execute(api, request)
+ return@withContext result.totalRecordCount
+ }
}
@Composable
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt
index 9a7d3d77..bea5d281 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt
@@ -59,7 +59,9 @@ import com.github.damontecres.wholphin.ui.playback.isForwardButton
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import timber.log.Timber
private const val DEBUG = false
@@ -350,7 +352,10 @@ fun CardGrid(
modifier = Modifier.align(Alignment.CenterVertically),
letterClicked = { letter ->
scope.launch(ExceptionHandler()) {
- val jumpPosition = letterPosition.invoke(letter)
+ val jumpPosition =
+ withContext(Dispatchers.IO) {
+ letterPosition.invoke(letter)
+ }
Timber.d("Alphabet jump to $jumpPosition")
if (jumpPosition >= 0) {
gridState.scrollToItem(jumpPosition)
From d76f829804c5cc8186d9a5fb2dc9d76a5a450b5c Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Sun, 2 Nov 2025 10:51:59 -0500
Subject: [PATCH 2/8] Option to show next up during credits/outros (#130)
Adds an option to the show the next up during the end credits (outro)
instead of at the very end of the video.
The next up pushes up from the bottom and the video scales down to fit
above.
Still a WIP though
### Sample screenshot
---
.../wholphin/preferences/AppPreference.kt | 26 ++++++-
.../preferences/AppPreferencesSerializer.kt | 1 +
.../wholphin/ui/playback/PlaybackPage.kt | 17 +++--
.../wholphin/ui/playback/PlaybackViewModel.kt | 67 +++++++++++--------
app/src/main/proto/WholphinDataStore.proto | 6 ++
app/src/main/res/values/preferences.xml | 5 ++
app/src/main/res/values/strings.xml | 2 +
7 files changed, 87 insertions(+), 37 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
index 2805cbbd..5ef7a763 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
@@ -602,6 +602,19 @@ sealed interface AppPreference {
summaryOn = R.string.enabled,
summaryOff = R.string.nav_drawer_switch_on_focus_summary_off,
)
+
+ val ShowNextUpTiming =
+ AppChoicePreference(
+ title = R.string.show_next_up_when,
+ defaultValue = ShowNextUpWhen.END_OF_PLAYBACK,
+ getter = { it.playbackPreferences.showNextUpWhen },
+ setter = { prefs, value ->
+ prefs.updatePlaybackPreferences { showNextUpWhen = value }
+ },
+ displayValues = R.array.show_next_up_when_options,
+ indexToValue = { ShowNextUpWhen.forNumber(it) },
+ valueToIndex = { it.number },
+ )
}
}
@@ -613,7 +626,6 @@ val basicPreferences =
listOf(
AppPreference.HomePageItems,
AppPreference.RewatchNextUp,
- AppPreference.CombineContinueNext,
AppPreference.PlayThemeMusic,
AppPreference.RememberSelectedTab,
AppPreference.ThemeColors,
@@ -625,11 +637,17 @@ val basicPreferences =
listOf(
AppPreference.SkipForward,
AppPreference.SkipBack,
- AppPreference.ControllerTimeout,
+ AppPreference.SkipBackOnResume,
+ ),
+ ),
+ PreferenceGroup(
+ title = R.string.next_up,
+ preferences =
+ listOf(
+ AppPreference.ShowNextUpTiming,
AppPreference.AutoPlayNextUp,
AppPreference.AutoPlayNextDelay,
AppPreference.PassOutProtection,
- AppPreference.SkipBackOnResume,
),
),
PreferenceGroup(
@@ -664,7 +682,9 @@ val advancedPreferences =
title = R.string.ui_interface,
preferences =
listOf(
+ AppPreference.CombineContinueNext,
AppPreference.NavDrawerSwitchOnFocus,
+ AppPreference.ControllerTimeout,
),
),
PreferenceGroup(
diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt
index 4fce1470..bb90e4a8 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt
@@ -43,6 +43,7 @@ class AppPreferencesSerializer
skipRecaps = AppPreference.SkipRecaps.defaultValue
passOutProtectionMs =
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
+ showNextUpWhen = AppPreference.ShowNextUpTiming.defaultValue
overrides =
PlaybackOverrides
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt
index 50a1d81b..ec9dc464 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt
@@ -69,9 +69,11 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus
+import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.seasonEpisode
import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID
@@ -162,7 +164,6 @@ fun PlaybackPage(
val scaledModifier =
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
val focusRequester = remember { FocusRequester() }
- val controllerFocusRequester = remember { FocusRequester() }
val playPauseState = rememberPlayPauseButtonState(player)
val seekBarState = rememberSeekBarState(player, scope)
@@ -215,9 +216,9 @@ fun PlaybackPage(
Box(
modifier
- .background(Color.Black),
+ .background(if (nextUp == null) Color.Black else MaterialTheme.colorScheme.background),
) {
- val playerSize by animateFloatAsState(if (nextUp == null) 1f else .66f)
+ val playerSize by animateFloatAsState(if (nextUp == null) 1f else .6f)
Box(
modifier =
Modifier
@@ -418,7 +419,13 @@ fun PlaybackPage(
// Next up episode
BackHandler(nextUp != null) {
- viewModel.navigationManager.goBack()
+ if (player.isPlaying) {
+ scope.launch(ExceptionHandler()) {
+ viewModel.cancelUpNextEpisode()
+ }
+ } else {
+ viewModel.navigationManager.goBack()
+ }
}
AnimatedVisibility(
nextUp != null,
@@ -470,7 +477,7 @@ fun PlaybackPage(
Modifier
.padding(8.dp)
// .height(128.dp)
- .fillMaxHeight(.5f)
+ .fillMaxHeight(1 - playerSize)
.fillMaxWidth(.66f)
.align(Alignment.BottomCenter)
.background(
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
index 1510830d..d1b523a6 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
@@ -33,6 +33,7 @@ import com.github.damontecres.wholphin.data.model.chooseStream
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
+import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.launchIO
@@ -97,12 +98,6 @@ import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
-enum class TranscodeType {
- DIRECT_PLAY,
- DIRECT_STREAM,
- TRANSCODE,
-}
-
data class StreamDecision(
val itemId: UUID,
val type: PlayMethod,
@@ -159,6 +154,7 @@ class PlaybackViewModel
val trickplay = MutableLiveData(null)
val chapters = MutableLiveData>(listOf())
val currentSegment = EqualityMutableLiveData(null)
+ private val autoSkippedSegments = mutableSetOf()
private lateinit var preferences: UserPreferences
private lateinit var deviceProfile: DeviceProfile
@@ -244,6 +240,7 @@ class PlaybackViewModel
): Boolean =
withContext(Dispatchers.IO) {
Timber.i("Playing ${base.id}")
+ autoSkippedSegments.clear()
if (base.type !in supportItemKinds) {
showToast(
context,
@@ -358,11 +355,9 @@ class PlaybackViewModel
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
subtitleIndex = subtitleIndex ?: TrackIndex.UNSPECIFIED,
)
- withContext(Dispatchers.Main) {
- this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse
- }
withContext(Dispatchers.Main) {
+ this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse
this@PlaybackViewModel.audioStreams.value = audioStreams
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
@@ -664,39 +659,53 @@ class PlaybackViewModel
.firstOrNull {
it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks
}
- if (currentSegment != null) {
+ if (currentSegment != null && autoSkippedSegments.add(currentSegment.id)) {
Timber.d(
"Found media segment for %s: %s, %s",
currentSegment.itemId,
currentSegment.id,
currentSegment.type,
)
- val behavior =
- when (currentSegment.type) {
- MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
- MediaSegmentType.PREVIEW -> prefs.skipPreviews
- MediaSegmentType.RECAP -> prefs.skipRecaps
- MediaSegmentType.OUTRO -> prefs.skipOutros
- MediaSegmentType.INTRO -> prefs.skipIntros
- MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE
+ val playlist = this@PlaybackViewModel.playlist.value
+
+ if (currentSegment.type == MediaSegmentType.OUTRO &&
+ prefs.showNextUpWhen == ShowNextUpWhen.DURING_CREDITS &&
+ playlist != null && playlist.hasNext()
+ ) {
+ val nextItem = playlist.peek()
+ Timber.v("Setting next up during outro to ${nextItem?.id}")
+ withContext(Dispatchers.Main) {
+ nextUp.value = nextItem
}
- withContext(Dispatchers.Main) {
- when (behavior) {
- SkipSegmentBehavior.AUTO_SKIP -> {
- this@PlaybackViewModel.currentSegment.value = null
- player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1)
+ } else {
+ val behavior =
+ when (currentSegment.type) {
+ MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
+ MediaSegmentType.PREVIEW -> prefs.skipPreviews
+ MediaSegmentType.RECAP -> prefs.skipRecaps
+ MediaSegmentType.OUTRO -> prefs.skipOutros
+ MediaSegmentType.INTRO -> prefs.skipIntros
+ MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE
}
+ withContext(Dispatchers.Main) {
+ when (behavior) {
+ SkipSegmentBehavior.AUTO_SKIP -> {
+ this@PlaybackViewModel.currentSegment.value = null
+ player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1)
+ }
- SkipSegmentBehavior.ASK_TO_SKIP -> {
- this@PlaybackViewModel.currentSegment.value = currentSegment
- }
+ SkipSegmentBehavior.ASK_TO_SKIP -> {
+ this@PlaybackViewModel.currentSegment.value =
+ currentSegment
+ }
- else -> {
- this@PlaybackViewModel.currentSegment.value = null
+ else -> {
+ this@PlaybackViewModel.currentSegment.value = null
+ }
}
}
}
- } else {
+ } else if (currentSegment == null) {
withContext(Dispatchers.Main) {
this@PlaybackViewModel.currentSegment.value = null
}
diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto
index f27f146a..f6023e49 100644
--- a/app/src/main/proto/WholphinDataStore.proto
+++ b/app/src/main/proto/WholphinDataStore.proto
@@ -3,6 +3,11 @@ syntax = "proto3";
option java_package = "com.github.damontecres.wholphin.preferences";
option java_multiple_files = true;
+enum ShowNextUpWhen{
+ END_OF_PLAYBACK = 0;
+ DURING_CREDITS = 1;
+}
+
enum SkipSegmentBehavior{
IGNORE = 0;
AUTO_SKIP = 1;
@@ -52,6 +57,7 @@ message PlaybackPreferences {
PlaybackOverrides overrides = 15;
int64 pass_out_protection_ms = 16;
PrefContentScale global_content_scale = 17;
+ ShowNextUpWhen show_next_up_when = 18;
}
message HomePagePreferences{
diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml
index bc8574f4..6e38778a 100644
--- a/app/src/main/res/values/preferences.xml
+++ b/app/src/main/res/values/preferences.xml
@@ -37,4 +37,9 @@
- Never use FFmpeg decoders
+
+ - At the end of playback
+ - During end credits/outro
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3e51cc61..f2ca9395 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -100,5 +100,7 @@
Use FFmpeg decoder module
Switch nav drawer pages on focus
Click to switch pages
+ Show next up
+ Next Up
From fe0f96d4137d2651481dbdbb63682a6f2df35a79 Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Sun, 2 Nov 2025 17:06:06 -0500
Subject: [PATCH 3/8] Revert nav drawer on focus #127 (#140)
Temporarily reverts #127 and reopens #95
It needs more refinement.
---
.../github/damontecres/wholphin/preferences/AppPreference.kt | 3 ++-
.../java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
index 5ef7a763..579c4e75 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
@@ -683,7 +683,8 @@ val advancedPreferences =
preferences =
listOf(
AppPreference.CombineContinueNext,
- AppPreference.NavDrawerSwitchOnFocus,
+ // Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
+// AppPreference.NavDrawerSwitchOnFocus,
AppPreference.ControllerTimeout,
),
),
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt
index f8f37703..bc18b47c 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt
@@ -253,7 +253,8 @@ fun NavDrawer(
}
}
}
- if (preferences.appPreferences.interfacePreferences.navDrawerSwitchOnFocus) {
+ // Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
+ if (false && preferences.appPreferences.interfacePreferences.navDrawerSwitchOnFocus) {
LaunchedEffect(derivedFocusedIndex) {
val index = derivedFocusedIndex
delay(600)
From 78322ab0a503f51de055ad6546eec60091d3db2b Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Sun, 2 Nov 2025 17:28:18 -0500
Subject: [PATCH 4/8] Fix deserialization error during login (#145)
Fixes #144
The error was occurring when querying for server information. The
endpoint used returns information about previous installations and seems
some may be missing a field marked as required in the API. This causes
the deserialization error.
Since the query is just to get the server's name for the UI, we can use
the public server info endpoint instead and also wrap it in a try-catch
since it's not absolutely required.
Additionally, this PR adds exceptions for cases when necessary
information is not available (user id, access token, etc).
---
.../wholphin/data/ServerRepository.kt | 22 +++++++++++++------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt
index 8975579f..9b910c78 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt
@@ -84,13 +84,15 @@ class ServerRepository
}
Timber.v("Changing user to ${user.name} on ${server.url}")
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
- val userDto =
- apiClient.userApi
- .getCurrentUser()
- .content
- val sysInfo by apiClient.systemApi.getSystemInfo()
-
- val updatedServer = server.copy(name = sysInfo.serverName)
+ val userDto by apiClient.userApi.getCurrentUser()
+ val updatedServer =
+ try {
+ val sysInfo by apiClient.systemApi.getPublicSystemInfo()
+ server.copy(name = sysInfo.serverName)
+ } catch (ex: Exception) {
+ Timber.w(ex, "Exception fetching public system info")
+ server
+ }
var updatedUser =
user.copy(
id = userDto.id,
@@ -171,8 +173,14 @@ class ServerRepository
}
if (user != null) {
changeUser(server, user)
+ } else {
+ throw IllegalArgumentException("Authentication result's user was null")
}
+ } else {
+ throw IllegalArgumentException("Authentication result's serverId not valid: ${authenticationResult.serverId}")
}
+ } else {
+ throw IllegalArgumentException("Authentication result's access token was null")
}
}
From d658ec16f3b2be411e57303e72ce67133453cb41 Mon Sep 17 00:00:00 2001
From: Damontecres
Date: Sun, 2 Nov 2025 17:33:51 -0500
Subject: [PATCH 5/8] Release v0.2.5
From 08577b8a31642850a32a5b46c5cf0fa17873f9bd Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Mon, 3 Nov 2025 11:50:47 -0500
Subject: [PATCH 6/8] Play theme song for movies (#149)
Closes #133
Fixes #142
Theme songs will play for both TV shows and movies, if available. Also,
if there are multiple theme songs, one will be picked randomly instead
of always playing the first.
Additionally, the music now waits to start until the UI has loaded.
---
.../wholphin/ui/detail/movie/MovieDetails.kt | 20 +++--
.../ui/detail/movie/MovieViewModel.kt | 27 ++++++-
.../ui/detail/series/SeriesDetails.kt | 19 ++---
.../ui/detail/series/SeriesOverview.kt | 18 ++---
.../ui/detail/series/SeriesViewModel.kt | 30 +------
.../wholphin/util/ThemeSongPlayer.kt | 78 ++++++++++++++-----
6 files changed, 121 insertions(+), 71 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt
index 301363d1..11ce4ba4 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt
@@ -39,6 +39,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
@@ -109,6 +110,15 @@ fun MovieDetails(
-> LoadingPage()
LoadingState.Success -> {
item?.let { movie ->
+ LifecycleStartEffect(destination.itemId) {
+ viewModel.maybePlayThemeSong(
+ destination.itemId,
+ preferences.appPreferences.interfacePreferences.playThemeSongs,
+ )
+ onStopOrDispose {
+ viewModel.release()
+ }
+ }
MovieDetailsContent(
preferences = preferences,
movie = movie,
@@ -118,10 +128,10 @@ fun MovieDetails(
trailers = trailers,
similar = similar,
onClickItem = {
- viewModel.navigationManager.navigateTo(it.destination())
+ viewModel.navigateTo(it.destination())
},
onClickPerson = {
- viewModel.navigationManager.navigateTo(
+ viewModel.navigateTo(
Destination.MediaItem(
it.id,
BaseItemKind.PERSON,
@@ -129,7 +139,7 @@ fun MovieDetails(
)
},
playOnClick = {
- viewModel.navigationManager.navigateTo(
+ viewModel.navigateTo(
Destination.Playback(
movie.id,
it.inWholeMilliseconds,
@@ -157,7 +167,7 @@ fun MovieDetails(
favorite = movie.data.userData?.isFavorite ?: false,
series = null,
sourceId = chosenStreams?.sourceId,
- navigateTo = viewModel.navigationManager::navigateTo,
+ navigateTo = viewModel::navigateTo,
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onChooseVersion = {
@@ -203,7 +213,7 @@ fun MovieDetails(
trailerOnClick = { trailer ->
when (trailer) {
is LocalTrailer ->
- viewModel.navigationManager.navigateTo(
+ viewModel.navigateTo(
Destination.Playback(
itemId = trailer.baseItem.id,
item = trailer.baseItem,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
index d51d9523..82671199 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
@@ -12,15 +12,18 @@ import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer
+import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.letNotEmpty
+import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
+import com.github.damontecres.wholphin.util.ThemeSongPlayer
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -40,9 +43,10 @@ class MovieViewModel
@Inject
constructor(
api: ApiClient,
- val navigationManager: NavigationManager,
+ private val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
+ private val themeSongPlayer: ThemeSongPlayer,
) : LoadingItemViewModel(api) {
private lateinit var itemId: UUID
@@ -181,4 +185,25 @@ class MovieViewModel
}
}
}
+
+ fun maybePlayThemeSong(
+ seriesId: UUID,
+ playThemeSongs: ThemeSongVolume,
+ ) {
+ viewModelScope.launchIO {
+ themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
+ addCloseable {
+ themeSongPlayer.stop()
+ }
+ }
+ }
+
+ fun release() {
+ themeSongPlayer.stop()
+ }
+
+ fun navigateTo(destination: Destination) {
+ release()
+ navigationManager.navigateTo(destination)
+ }
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt
index 85ff5a9d..fb2f7a04 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt
@@ -83,15 +83,6 @@ fun SeriesDetails(
modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel(),
) {
- LifecycleStartEffect(destination.itemId) {
- viewModel.maybePlayThemeSong(
- destination.itemId,
- preferences.appPreferences.interfacePreferences.playThemeSongs,
- )
- onStopOrDispose {
- viewModel.release()
- }
- }
LaunchedEffect(Unit) {
viewModel.init(preferences, destination.itemId, destination.item, null)
}
@@ -113,6 +104,16 @@ fun SeriesDetails(
-> LoadingPage()
LoadingState.Success -> {
item?.let { item ->
+ LifecycleStartEffect(destination.itemId) {
+ viewModel.maybePlayThemeSong(
+ destination.itemId,
+ preferences.appPreferences.interfacePreferences.playThemeSongs,
+ )
+ onStopOrDispose {
+ viewModel.release()
+ }
+ }
+
val played = item.data.userData?.played ?: false
SeriesDetailsContent(
preferences = preferences,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt
index 4502ef07..75ea4555 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt
@@ -72,15 +72,6 @@ fun SeriesOverview(
viewModel: SeriesViewModel = hiltViewModel(),
initialSeasonEpisode: SeasonEpisodeIds? = null,
) {
- LifecycleStartEffect(destination.itemId) {
- viewModel.maybePlayThemeSong(
- destination.itemId,
- preferences.appPreferences.interfacePreferences.playThemeSongs,
- )
- onStopOrDispose {
- viewModel.release()
- }
- }
val firstItemFocusRequester = remember { FocusRequester() }
val episodeRowFocusRequester = remember { FocusRequester() }
@@ -168,6 +159,15 @@ fun SeriesOverview(
LoadingState.Success -> {
series?.let { series ->
LaunchedEffect(Unit) { episodeRowFocusRequester.tryRequestFocus() }
+ LifecycleStartEffect(destination.itemId) {
+ viewModel.maybePlayThemeSong(
+ destination.itemId,
+ preferences.appPreferences.interfacePreferences.playThemeSongs,
+ )
+ onStopOrDispose {
+ viewModel.release()
+ }
+ }
fun buildMoreForEpisode(
ep: BaseItem,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt
index 04da9868..0937006d 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt
@@ -2,10 +2,8 @@ package com.github.damontecres.wholphin.ui.detail.series
import android.content.Context
import android.widget.Toast
-import androidx.annotation.OptIn
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
-import androidx.media3.common.util.UnstableApi
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository
@@ -30,7 +28,6 @@ import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.ThemeSongPlayer
-import com.github.damontecres.wholphin.util.profile.Codec
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -41,7 +38,6 @@ import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
-import org.jellyfin.sdk.api.client.extensions.universalAudioApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields
@@ -141,34 +137,14 @@ class SeriesViewModel
/**
* If the series has a theme song & app settings allow, play it
*/
- @OptIn(UnstableApi::class)
fun maybePlayThemeSong(
seriesId: UUID,
playThemeSongs: ThemeSongVolume,
) {
viewModelScope.launchIO {
- val themeSongs = api.libraryApi.getThemeSongs(seriesId).content
- themeSongs.items.firstOrNull()?.let { theme ->
- theme.mediaSources?.firstOrNull()?.let { source ->
- val url =
- api.universalAudioApi.getUniversalAudioStreamUrl(
- theme.id,
- container =
- listOf(
- Codec.Audio.OPUS,
- Codec.Audio.MP3,
- Codec.Audio.AAC,
- Codec.Audio.FLAC,
- ),
- )
- Timber.v("Found theme song for series $seriesId")
- withContext(Dispatchers.Main) {
- themeSongPlayer.play(playThemeSongs, url)
- addCloseable {
- themeSongPlayer.stop()
- }
- }
- }
+ themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
+ addCloseable {
+ themeSongPlayer.stop()
}
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt
index 02160e65..1327b6f2 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt
@@ -10,31 +10,77 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import com.github.damontecres.wholphin.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
+import com.github.damontecres.wholphin.util.profile.Codec
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
+import org.jellyfin.sdk.api.client.ApiClient
+import org.jellyfin.sdk.api.client.extensions.libraryApi
+import org.jellyfin.sdk.api.client.extensions.universalAudioApi
+import timber.log.Timber
+import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Simple service to play theme song music
*/
+@OptIn(UnstableApi::class)
@Singleton
class ThemeSongPlayer
@Inject
constructor(
@param:ApplicationContext private val context: Context,
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
+ private val api: ApiClient,
) {
- private var player: Player? = null
+ private val player: Player by lazy {
+ ExoPlayer
+ .Builder(context)
+ .setMediaSourceFactory(
+ DefaultMediaSourceFactory(
+ OkHttpDataSource.Factory(authOkHttpClient),
+ ),
+ ).build()
+ }
- @OptIn(UnstableApi::class)
- fun play(
+ suspend fun playThemeFor(
+ itemId: UUID,
volume: ThemeSongVolume,
+ ): Boolean =
+ withContext(Dispatchers.IO) {
+ if (volume == ThemeSongVolume.DISABLED || volume == ThemeSongVolume.UNRECOGNIZED) {
+ return@withContext false
+ }
+ val themeSongs by api.libraryApi.getThemeSongs(itemId)
+ return@withContext themeSongs.items.randomOrNull()?.let { theme ->
+ val url =
+ api.universalAudioApi.getUniversalAudioStreamUrl(
+ theme.id,
+ container =
+ listOf(
+ Codec.Audio.OPUS,
+ Codec.Audio.MP3,
+ Codec.Audio.AAC,
+ Codec.Audio.FLAC,
+ ),
+ )
+ Timber.v("Found theme song for $itemId")
+ withContext(Dispatchers.Main) {
+ play(volume, url)
+ }
+ true
+ } ?: false
+ }
+
+ private fun play(
+ volumeLevel: ThemeSongVolume,
url: String,
) {
stop()
val volumeLevel =
- when (volume) {
+ when (volumeLevel) {
ThemeSongVolume.UNRECOGNIZED,
ThemeSongVolume.DISABLED,
-> return
@@ -45,24 +91,16 @@ class ThemeSongPlayer
ThemeSongVolume.HIGH -> .5f
ThemeSongVolume.HIGHEST -> 75f
}
- val player =
- ExoPlayer
- .Builder(context)
- .setMediaSourceFactory(
- DefaultMediaSourceFactory(
- OkHttpDataSource.Factory(authOkHttpClient),
- ),
- ).build()
- .apply {
- this.volume = volumeLevel
- playWhenReady = true
- }
- player.setMediaItem(MediaItem.fromUri(url))
- player.prepare()
- this.player = player
+ player.apply {
+ volume = volumeLevel
+ setMediaItem(MediaItem.fromUri(url))
+ prepare()
+ play()
+ }
}
fun stop() {
- player?.release()
+ Timber.v("Stopping theme song")
+ player.stop()
}
}
From 40267be633e76828cb2e7184c01e52fb0428f914 Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Mon, 3 Nov 2025 12:15:41 -0500
Subject: [PATCH 7/8] Remember library/collection sorting (#150)
Closes #148
Closes #83
Remember the chosen sort order & direction per user profile on device.
Also adds a few more sort options.
This is only saved on the device and not saved/loaded from the server.
---
.../6.json | 303 ++++++++++++++++++
.../damontecres/wholphin/data/AppDatabase.kt | 22 +-
.../wholphin/data/LibraryDisplayInfoDao.kt | 29 ++
.../wholphin/data/model/LibraryDisplayInfo.kt | 40 +++
.../wholphin/hilt/DatabaseModule.kt | 5 +
.../ui/components/CollectionFolderGrid.kt | 82 +++--
.../wholphin/ui/data/SortAndDirection.kt | 15 +
.../wholphin/ui/detail/ItemViewModel.kt | 20 +-
.../wholphin/ui/detail/PlaylistDetails.kt | 2 +-
.../ui/detail/movie/MovieViewModel.kt | 8 +-
.../ui/detail/series/SeriesViewModel.kt | 8 +-
app/src/main/res/values/strings.xml | 3 +
12 files changed, 486 insertions(+), 51 deletions(-)
create mode 100644 app/schemas/com.github.damontecres.wholphin.data.AppDatabase/6.json
create mode 100644 app/src/main/java/com/github/damontecres/wholphin/data/LibraryDisplayInfoDao.kt
create mode 100644 app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt
diff --git a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/6.json b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/6.json
new file mode 100644
index 00000000..40c938fb
--- /dev/null
+++ b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/6.json
@@ -0,0 +1,303 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 6,
+ "identityHash": "24390d64259f19e0e67ecfb827b974fb",
+ "entities": [
+ {
+ "tableName": "servers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "users",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "rowId",
+ "columnName": "rowId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "serverId",
+ "columnName": "serverId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accessToken",
+ "columnName": "accessToken",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "rowId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_users_id_serverId",
+ "unique": true,
+ "columnNames": [
+ "id",
+ "serverId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
+ },
+ {
+ "name": "index_users_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
+ },
+ {
+ "name": "index_users_serverId",
+ "unique": false,
+ "columnNames": [
+ "serverId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "servers",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "serverId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ItemPlayback",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "rowId",
+ "columnName": "rowId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "itemId",
+ "columnName": "itemId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sourceId",
+ "columnName": "sourceId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "audioIndex",
+ "columnName": "audioIndex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subtitleIndex",
+ "columnName": "subtitleIndex",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "rowId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ItemPlayback_userId_itemId",
+ "unique": true,
+ "columnNames": [
+ "userId",
+ "itemId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "users",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "rowId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "NavDrawerPinnedItem",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "itemId",
+ "columnName": "itemId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "userId",
+ "itemId"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "users",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "rowId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LibraryDisplayInfo",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "itemId",
+ "columnName": "itemId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sort",
+ "columnName": "sort",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "direction",
+ "columnName": "direction",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "userId",
+ "itemId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_LibraryDisplayInfo_userId_itemId",
+ "unique": true,
+ "columnNames": [
+ "userId",
+ "itemId"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "users",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "rowId"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '24390d64259f19e0e67ecfb827b974fb')"
+ ]
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt
index ad9fb3a7..305a3146 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt
@@ -10,17 +10,21 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
+import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
+import org.jellyfin.sdk.model.api.ItemSortBy
+import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
@Database(
- entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class],
- version = 5,
+ entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
+ version = 6,
exportSchema = true,
autoMigrations = [
AutoMigration(3, 4),
AutoMigration(4, 5),
+ AutoMigration(5, 6),
],
)
@TypeConverters(Converters::class)
@@ -30,6 +34,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun itemPlaybackDao(): ItemPlaybackDao
abstract fun serverPreferencesDao(): ServerPreferencesDao
+
+ abstract fun libraryDisplayInfoDao(): LibraryDisplayInfoDao
}
class Converters {
@@ -38,6 +44,18 @@ class Converters {
@TypeConverter
fun convertToUUID(str: String): UUID = str.toUUID()
+
+ @TypeConverter
+ fun convertItemSortBy(sort: ItemSortBy): String = sort.serialName
+
+ @TypeConverter
+ fun convertItemSortBy(sort: String): ItemSortBy? = ItemSortBy.fromNameOrNull(sort)
+
+ @TypeConverter
+ fun convertSortOrder(sort: SortOrder): String = sort.serialName
+
+ @TypeConverter
+ fun convertSortOrder(sort: String): SortOrder? = SortOrder.fromNameOrNull(sort)
}
object Migrations {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/LibraryDisplayInfoDao.kt b/app/src/main/java/com/github/damontecres/wholphin/data/LibraryDisplayInfoDao.kt
new file mode 100644
index 00000000..8e1b1ef2
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/wholphin/data/LibraryDisplayInfoDao.kt
@@ -0,0 +1,29 @@
+package com.github.damontecres.wholphin.data
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.github.damontecres.wholphin.data.model.JellyfinUser
+import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
+import java.util.UUID
+
+@Dao
+interface LibraryDisplayInfoDao {
+ fun getItem(
+ user: JellyfinUser,
+ itemId: UUID,
+ ): LibraryDisplayInfo? = getItem(user.rowId, itemId)
+
+ @Query("SELECT * from LibraryDisplayInfo WHERE userId=:userId AND itemId=:itemId")
+ fun getItem(
+ userId: Int,
+ itemId: UUID,
+ ): LibraryDisplayInfo?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun saveItem(item: LibraryDisplayInfo): Long
+
+ @Query("SELECT * from LibraryDisplayInfo WHERE userId=:userId")
+ fun getItems(userId: Int): List
+}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt
new file mode 100644
index 00000000..d428f521
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt
@@ -0,0 +1,40 @@
+@file:UseSerializers(UUIDSerializer::class)
+
+package com.github.damontecres.wholphin.data.model
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Ignore
+import androidx.room.Index
+import com.github.damontecres.wholphin.ui.data.SortAndDirection
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.UseSerializers
+import org.jellyfin.sdk.model.api.ItemSortBy
+import org.jellyfin.sdk.model.api.SortOrder
+import org.jellyfin.sdk.model.serializer.UUIDSerializer
+import java.util.UUID
+
+@Entity(
+ foreignKeys = [
+ ForeignKey(
+ entity = JellyfinUser::class,
+ parentColumns = arrayOf("rowId"),
+ childColumns = arrayOf("userId"),
+ onDelete = ForeignKey.CASCADE,
+ onUpdate = ForeignKey.CASCADE,
+ ),
+ ],
+ primaryKeys = ["userId", "itemId"],
+ indices = [Index("userId", "itemId", unique = true)],
+)
+@Serializable
+data class LibraryDisplayInfo(
+ val userId: Int,
+ val itemId: UUID,
+ val sort: ItemSortBy,
+ val direction: SortOrder,
+) {
+ @Ignore @Transient
+ val sortAndDirection = SortAndDirection(sort, direction)
+}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/hilt/DatabaseModule.kt b/app/src/main/java/com/github/damontecres/wholphin/hilt/DatabaseModule.kt
index ab1fb0e5..441a9033 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/hilt/DatabaseModule.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/hilt/DatabaseModule.kt
@@ -9,6 +9,7 @@ import androidx.room.Room
import com.github.damontecres.wholphin.data.AppDatabase
import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.JellyfinServerDao
+import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
import com.github.damontecres.wholphin.data.Migrations
import com.github.damontecres.wholphin.data.ServerPreferencesDao
import com.github.damontecres.wholphin.preferences.AppPreferences
@@ -51,6 +52,10 @@ object DatabaseModule {
@Singleton
fun serverPreferencesDao(db: AppDatabase): ServerPreferencesDao = db.serverPreferencesDao()
+ @Provides
+ @Singleton
+ fun libraryDisplayInfoDao(db: AppDatabase): LibraryDisplayInfoDao = db.libraryDisplayInfoDao()
+
@Provides
@Singleton
fun userPreferencesDataStore(
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
index 8d5139ca..38f34a07 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
@@ -26,8 +26,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
+import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
+import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
+import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
@@ -62,6 +65,8 @@ class CollectionFolderViewModel
@Inject
constructor(
api: ApiClient,
+ private val serverRepository: ServerRepository,
+ private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
) : ItemViewModel(api) {
val loading = MutableLiveData(LoadingState.Loading)
val pager = MutableLiveData>(listOf())
@@ -71,7 +76,7 @@ class CollectionFolderViewModel
fun init(
itemId: UUID?,
potential: BaseItem?,
- sortAndDirection: SortAndDirection,
+ initialSortAndDirection: SortAndDirection?,
recursive: Boolean,
filter: GetItemsFilter,
): Job =
@@ -82,12 +87,41 @@ class CollectionFolderViewModel
) + Dispatchers.IO,
) {
if (itemId != null) {
- fetchItem(itemId, potential)
+ fetchItem(itemId)
+
+ val sortAndDirection =
+ if (initialSortAndDirection == null) {
+ serverRepository.currentUser?.let { user ->
+ libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
+ } ?: SortAndDirection.DEFAULT
+ } else {
+ SortAndDirection.DEFAULT
+ }
+ loadResults(sortAndDirection, recursive, filter)
}
- loadResults(sortAndDirection, recursive, filter)
}
- fun loadResults(
+ fun onSortChange(
+ sortAndDirection: SortAndDirection,
+ recursive: Boolean,
+ filter: GetItemsFilter,
+ ) {
+ serverRepository.currentUser?.let { user ->
+ viewModelScope.launch(Dispatchers.IO) {
+ val libraryDisplayInfo =
+ LibraryDisplayInfo(
+ userId = user.rowId,
+ itemId = itemId,
+ sort = sortAndDirection.sort,
+ direction = sortAndDirection.direction,
+ )
+ libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
+ }
+ }
+ loadResults(sortAndDirection, recursive, filter)
+ }
+
+ private fun loadResults(
sortAndDirection: SortAndDirection,
recursive: Boolean,
filter: GetItemsFilter,
@@ -126,17 +160,25 @@ class CollectionFolderViewModel
recursive = recursive,
excludeItemIds = item?.let { listOf(item.id) },
sortBy =
- listOf(
- sortAndDirection.sort,
- ItemSortBy.SORT_NAME,
- ItemSortBy.PRODUCTION_YEAR,
- ),
+ buildList {
+ add(sortAndDirection.sort)
+ if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
+ add(ItemSortBy.SORT_NAME)
+ }
+ if (item?.data?.collectionType == CollectionType.MOVIES) {
+ add(ItemSortBy.PRODUCTION_YEAR)
+ }
+ },
sortOrder =
- listOf(
- sortAndDirection.direction,
- SortOrder.ASCENDING,
- SortOrder.ASCENDING,
- ),
+ buildList {
+ add(sortAndDirection.direction)
+ if (sortAndDirection.sort != ItemSortBy.SORT_NAME) {
+ add(SortOrder.ASCENDING)
+ }
+ if (item?.data?.collectionType == CollectionType.MOVIES) {
+ add(SortOrder.ASCENDING)
+ }
+ },
fields = SlimItemFields,
),
)
@@ -198,18 +240,14 @@ fun CollectionFolderGrid(
onClickItem: (BaseItem) -> Unit,
modifier: Modifier = Modifier,
viewModel: CollectionFolderViewModel = hiltViewModel(),
- initialSortAndDirection: SortAndDirection =
- SortAndDirection(
- ItemSortBy.SORT_NAME,
- SortOrder.ASCENDING,
- ),
+ initialSortAndDirection: SortAndDirection? = null,
showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
) {
OneTimeLaunchedEffect {
viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter)
}
- val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection)
+ val sortAndDirection by viewModel.sortAndDirection.observeAsState()
val filter by viewModel.filter.observeAsState(initialFilter)
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val item by viewModel.item.observeAsState()
@@ -226,11 +264,11 @@ fun CollectionFolderGrid(
preferences,
item,
pager,
- sortAndDirection = sortAndDirection,
+ sortAndDirection = sortAndDirection!!,
modifier = modifier,
onClickItem = onClickItem,
onSortChange = {
- viewModel.loadResults(it, recursive, filter)
+ viewModel.onSortChange(it, recursive, filter)
},
showTitle = showTitle,
positionCallback = positionCallback,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt
index 945ed053..ac0808a5 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt
@@ -10,6 +10,14 @@ data class SortAndDirection(
val direction: SortOrder,
) {
fun flip() = copy(direction = direction.flip())
+
+ companion object {
+ val DEFAULT =
+ SortAndDirection(
+ ItemSortBy.SORT_NAME,
+ SortOrder.ASCENDING,
+ )
+ }
}
fun SortOrder.flip() = if (this == SortOrder.ASCENDING) SortOrder.DESCENDING else SortOrder.ASCENDING
@@ -20,6 +28,9 @@ val MovieSortOptions =
ItemSortBy.PREMIERE_DATE,
ItemSortBy.DATE_CREATED,
ItemSortBy.DATE_PLAYED,
+ ItemSortBy.COMMUNITY_RATING,
+ ItemSortBy.CRITIC_RATING,
+ ItemSortBy.PLAY_COUNT,
ItemSortBy.RANDOM,
)
@@ -30,6 +41,7 @@ val SeriesSortOptions =
ItemSortBy.DATE_CREATED,
ItemSortBy.DATE_LAST_CONTENT_ADDED,
ItemSortBy.DATE_PLAYED,
+ ItemSortBy.COMMUNITY_RATING,
ItemSortBy.RANDOM,
)
@@ -50,5 +62,8 @@ fun getStringRes(sort: ItemSortBy): Int =
ItemSortBy.DATE_LAST_CONTENT_ADDED -> R.string.sort_by_date_episode_added
ItemSortBy.DATE_PLAYED -> R.string.sort_by_date_played
ItemSortBy.RANDOM -> R.string.sort_by_random
+ ItemSortBy.COMMUNITY_RATING -> R.string.community_rating
+ ItemSortBy.CRITIC_RATING -> R.string.critic_rating
+ ItemSortBy.PLAY_COUNT -> R.string.play_count
else -> throw IllegalArgumentException("Unsupported sort option: $sort")
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt
index 8a15d2f9..fec6162a 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/ItemViewModel.kt
@@ -25,21 +25,11 @@ abstract class ItemViewModel(
val api: ApiClient,
) : ViewModel() {
val item = MutableLiveData(null)
+ lateinit var itemId: UUID
- suspend fun fetchItem(
- itemId: UUID,
- potential: BaseItem?,
- ): BaseItem =
+ suspend fun fetchItem(itemId: UUID): BaseItem =
withContext(Dispatchers.IO) {
-// val fetchedItem =
-// when {
-// item.value == null && potential?.id == itemId -> potential
-// item.value?.id == itemId -> item.value
-// else -> {
-// val it = api.userLibraryApi.getItem(itemId).content
-// BaseItem.from(it, api)
-// }
-// }
+ this@ItemViewModel.itemId = itemId
val it = api.userLibraryApi.getItem(itemId).content
val fetchedItem = BaseItem.from(it, api)
return@withContext fetchedItem.let {
@@ -89,10 +79,8 @@ abstract class LoadingItemViewModel(
open suspend fun fetchAndSetItem(itemId: UUID): BaseItem =
withContext(Dispatchers.IO) {
- val fetchedItem = api.userLibraryApi.getItem(itemId).content
- val item = BaseItem.from(fetchedItem, api)
+ val item = fetchItem(itemId)
withContext(Dispatchers.Main) {
- this@LoadingItemViewModel.item.value = item
loading.value = LoadingState.Success
}
return@withContext item
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
index 54551834..49538cca 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
@@ -102,7 +102,7 @@ class PlaylistViewModel
Dispatchers.IO +
LoadingExceptionHandler(loading, "Failed to fetch playlist $playlistId"),
) {
- val playlist = fetchItem(playlistId, null)
+ val playlist = fetchItem(playlistId)
val request =
GetPlaylistItemsRequest(
playlistId = playlist.id,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
index 82671199..a27507ca 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
@@ -48,17 +48,14 @@ class MovieViewModel
val itemPlaybackRepository: ItemPlaybackRepository,
private val themeSongPlayer: ThemeSongPlayer,
) : LoadingItemViewModel(api) {
- private lateinit var itemId: UUID
-
val trailers = MutableLiveData>(listOf())
val people = MutableLiveData>(listOf())
val chapters = MutableLiveData>(listOf())
val similar = MutableLiveData>()
val chosenStreams = MutableLiveData(null)
- fun init(itemId: UUID): Job? {
- this.itemId = itemId
- return viewModelScope.launch(
+ fun init(itemId: UUID): Job? =
+ viewModelScope.launch(
Dispatchers.IO +
LoadingExceptionHandler(
loading,
@@ -124,7 +121,6 @@ class MovieViewModel
}
}
}
- }
fun setWatched(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt
index 0937006d..8acb295e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt
@@ -84,7 +84,7 @@ class SeriesViewModel
"Error loading series $seriesId",
) + Dispatchers.IO,
) {
- val item = fetchItem(seriesId, potential)
+ val item = fetchItem(seriesId)
val seasons = getSeasons(item)
// If a particular season was requested, fetch those episodes, otherwise get the first season
@@ -263,7 +263,7 @@ class SeriesViewModel
if (listIndex != null) {
refreshEpisode(itemId, listIndex)
} else {
- fetchItem(seriesId, null)
+ fetchItem(seriesId)
}
}
@@ -272,7 +272,7 @@ class SeriesViewModel
played: Boolean,
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
setWatched(seasonId, played, null)
- val series = fetchItem(seriesId, null)
+ val series = fetchItem(seriesId)
val seasons = getSeasons(series)
this@SeriesViewModel.seasons.setValueOnMain(seasons)
}
@@ -284,7 +284,7 @@ class SeriesViewModel
} else {
api.playStateApi.markUnplayedItem(seriesId)
}
- val series = fetchItem(seriesId, null)
+ val series = fetchItem(seriesId)
val seasons = getSeasons(series)
this@SeriesViewModel.seasons.setValueOnMain(seasons)
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f2ca9395..67fd8735 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -102,5 +102,8 @@
Click to switch pages
Show next up
Next Up
+ Community Rating
+ Critic Rating
+ Play Count
From 2840fa3f2ac1c698715ef7d59037c009a99085b1 Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:38:12 -0500
Subject: [PATCH 8/8] Extract strings into resources (#151)
Move all strings into resource files to make future translating easier
No user facing changes in this PR
---
.../damontecres/wholphin/hilt/AppModule.kt | 7 +-
.../wholphin/preferences/AppPreference.kt | 49 +++-
.../wholphin/ui/cards/ChapterRow.kt | 4 +-
.../wholphin/ui/cards/PersonRow.kt | 4 +-
.../ui/components/CollectionFolderGrid.kt | 9 +-
.../wholphin/ui/components/Dialogs.kt | 20 +-
.../wholphin/ui/components/ErrorMessage.kt | 6 +-
.../wholphin/ui/components/LoadingRow.kt | 6 +-
.../ui/components/RecommendedMovie.kt | 12 +-
.../ui/components/RecommendedTvShow.kt | 14 +-
.../wholphin/ui/data/ItemDetailsDialogInfo.kt | 32 ++-
.../wholphin/ui/detail/CardGrid.kt | 28 +-
.../ui/detail/CollectionFolderLiveTv.kt | 6 +-
.../ui/detail/CollectionFolderMovie.kt | 10 +-
.../wholphin/ui/detail/CollectionFolderTv.kt | 9 +-
.../wholphin/ui/detail/DetailUtils.kt | 32 ++-
.../wholphin/ui/detail/FavoritesPage.kt | 9 +-
.../wholphin/ui/detail/PersonPage.kt | 28 +-
.../wholphin/ui/detail/PlaylistDetails.kt | 9 +-
.../wholphin/ui/detail/livetv/DvrSchedule.kt | 6 +-
.../ui/detail/livetv/LiveTvViewModel.kt | 6 +-
.../ui/detail/livetv/ProgramDialog.kt | 26 +-
.../wholphin/ui/detail/movie/MovieDetails.kt | 9 +-
.../ui/detail/movie/MovieDetailsHeader.kt | 4 +-
.../ui/detail/movie/MovieViewModel.kt | 6 +-
.../ui/detail/series/FocusedEpisodeFooter.kt | 2 +-
.../ui/detail/series/SeriesDetails.kt | 24 +-
.../ui/detail/series/SeriesOverview.kt | 14 +-
.../ui/detail/series/SeriesOverviewContent.kt | 6 +-
.../damontecres/wholphin/ui/main/HomePage.kt | 20 +-
.../wholphin/ui/main/HomeSection.kt | 30 ---
.../wholphin/ui/main/HomeViewModel.kt | 17 +-
.../wholphin/ui/main/SearchPage.kt | 16 +-
.../damontecres/wholphin/ui/nav/NavDrawer.kt | 6 +-
.../ui/playback/DownloadSubtitlesDialog.kt | 17 +-
.../wholphin/ui/playback/Models.kt | 6 +-
.../wholphin/ui/playback/NextUpEpisode.kt | 4 +-
.../wholphin/ui/playback/PlaybackControls.kt | 20 +-
.../wholphin/ui/playback/PlaybackOverlay.kt | 12 +-
.../wholphin/ui/playback/PlaybackPage.kt | 5 +-
.../wholphin/ui/playback/PlaybackViewModel.kt | 3 +-
.../ui/preferences/PreferencesContent.kt | 8 +-
.../wholphin/ui/setup/InstallUpdatePage.kt | 8 +-
.../wholphin/ui/setup/ServerList.kt | 10 +-
.../wholphin/ui/setup/SwitchServerContent.kt | 12 +-
.../wholphin/ui/setup/SwitchUserContent.kt | 4 +-
.../damontecres/wholphin/ui/setup/UserList.kt | 11 +-
.../damontecres/wholphin/util/Constants.kt | 8 -
.../wholphin/util/CrashReportSenderFactory.kt | 2 +-
.../damontecres/wholphin/util/Formatting.kt | 14 +
app/src/main/res/values/preferences.xml | 48 ++++
app/src/main/res/values/strings.xml | 244 +++++++++++-------
52 files changed, 616 insertions(+), 306 deletions(-)
delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeSection.kt
diff --git a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt
index c1f9da80..34219c58 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt
@@ -5,6 +5,7 @@ import android.content.Context
import android.provider.Settings
import androidx.datastore.core.DataStore
import com.github.damontecres.wholphin.BuildConfig
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
@@ -47,9 +48,11 @@ annotation class IoCoroutineScope
object AppModule {
@Provides
@Singleton
- fun clientInfo(): ClientInfo =
+ fun clientInfo(
+ @ApplicationContext context: Context,
+ ): ClientInfo =
ClientInfo(
- name = "Wholphin",
+ name = context.getString(R.string.app_name),
version = BuildConfig.VERSION_NAME,
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
index 579c4e75..11554506 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
@@ -70,7 +70,11 @@ sealed interface AppPreference {
},
summarizer = { value ->
if (value != null) {
- "$value seconds"
+ WholphinApplication.instance.resources.getQuantityString(
+ R.plurals.seconds,
+ value.toInt(),
+ value.toInt(),
+ )
} else {
null
}
@@ -95,7 +99,11 @@ sealed interface AppPreference {
},
summarizer = { value ->
if (value != null) {
- "$value seconds"
+ WholphinApplication.instance.resources.getQuantityString(
+ R.plurals.seconds,
+ value.toInt(),
+ value.toInt(),
+ )
} else {
null
}
@@ -137,7 +145,14 @@ sealed interface AppPreference {
setter = { prefs, 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 =
@@ -243,7 +258,7 @@ sealed interface AppPreference {
},
summarizer = { value ->
if (value == 0L) {
- "Disabled"
+ WholphinApplication.instance.getString(R.string.disabled)
} else {
"${value}s"
}
@@ -262,10 +277,16 @@ sealed interface AppPreference {
prefs.updatePlaybackPreferences { autoPlayNextDelaySeconds = value }
},
summarizer = { value ->
- if (value == 0L) {
- "Immediate"
+ if (value == null) {
+ ""
+ } else if (value == 0L) {
+ WholphinApplication.instance.getString(R.string.immediate)
} else {
- "$value seconds"
+ WholphinApplication.instance.resources.getQuantityString(
+ R.plurals.seconds,
+ value.toInt(),
+ value.toInt(),
+ )
}
},
)
@@ -284,10 +305,16 @@ sealed interface AppPreference {
}
},
summarizer = { value ->
- if (value == 0L) {
- "Disabled"
+ if (value == null) {
+ ""
+ } else if (value == 0L) {
+ WholphinApplication.instance.getString(R.string.disabled)
} else {
- "$value hours"
+ WholphinApplication.instance.resources.getQuantityString(
+ R.plurals.hours,
+ value.toInt(),
+ value.toInt(),
+ )
}
},
)
@@ -485,7 +512,7 @@ sealed interface AppPreference {
val SkipCommercials =
AppChoicePreference(
- title = R.string.skip_comercials_behavior,
+ title = R.string.skip_commercials_behavior,
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
getter = { it.playbackPreferences.skipCommercials },
setter = { prefs, value ->
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt
index 2468f7b1..1c206990 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt
@@ -11,9 +11,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusRestorer
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.Chapter
@Composable
@@ -28,7 +30,7 @@ fun ChapterRow(
modifier = modifier,
) {
Text(
- text = "Chapters",
+ text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt
index 90f0f2ef..63730ca7 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt
@@ -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.focusRestorer
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.ui.ifElse
@@ -34,7 +36,7 @@ fun PersonRow(
modifier = modifier,
) {
Text(
- text = "People",
+ text = stringResource(R.string.people),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
index 38f34a07..31bd0245 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt
@@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.components
+import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -19,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -26,6 +28,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
import com.github.damontecres.wholphin.data.ServerRepository
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.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -65,6 +69,7 @@ class CollectionFolderViewModel
@Inject
constructor(
api: ApiClient,
+ @param:ApplicationContext private val context: Context,
private val serverRepository: ServerRepository,
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
) : ItemViewModel(api) {
@@ -83,7 +88,7 @@ class CollectionFolderViewModel
viewModelScope.launch(
LoadingExceptionHandler(
loading,
- "Error loading collection $itemId",
+ context.getString(R.string.error_loading_collection, itemId),
) + Dispatchers.IO,
) {
if (itemId != null) {
@@ -292,7 +297,7 @@ fun CollectionFolderGridContent(
showTitle: Boolean = true,
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 =
when (item?.data?.collectionType) {
CollectionType.MOVIES -> MovieSortOptions
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt
index 2c639bff..b2363059 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt
@@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.components
+import android.content.Context
import android.view.KeyEvent
import androidx.annotation.StringRes
import androidx.compose.foundation.background
@@ -462,12 +463,13 @@ fun ConfirmDialogContent(
}
fun chooseVersionParams(
+ context: Context,
sources: List,
onClick: (Int) -> Unit,
): DialogParams =
DialogParams(
fromLongClick = false,
- title = "Play Version",
+ title = context.getString(R.string.choose_stream, context.getString(R.string.version)),
items =
sources.filter { it.id.isNotNullOrBlank() }.mapIndexed { index, source ->
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(
+ context: Context,
streams: List,
type: MediaStreamType,
onClick: (Int) -> Unit,
): DialogParams =
DialogParams(
fromLongClick = false,
- title = "Choose ${type.serialName}", // TODO
+ title = context.getString(R.string.choose_stream, context.getString(resourceFor(type))),
items =
buildList {
if (type == MediaStreamType.SUBTITLE) {
add(
DialogItem(
headlineContent = {
- Text(text = "None")
+ Text(text = stringResource(R.string.none))
},
supportingContent = {
},
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ErrorMessage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ErrorMessage.kt
index 14e0fe31..f833e228 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ErrorMessage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ErrorMessage.kt
@@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
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.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -50,7 +52,7 @@ fun ErrorMessage(
modifier = modifier.padding(16.dp),
) {
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,
style = MaterialTheme.typography.titleMedium,
)
@@ -60,7 +62,7 @@ fun ErrorMessage(
},
) {
Text(
- text = "Send Logs",
+ text = stringResource(R.string.send_app_logs),
)
}
message?.let {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt
index da4fa30d..593f2db3 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LoadingRow.kt
@@ -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.graphics.Color
+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.Text
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.ItemRow
@@ -70,7 +72,7 @@ fun LoadingRow(
->
LoadingRowPlaceholder(
title = title,
- message = "Loading...",
+ message = stringResource(R.string.loading),
modifier = modifier,
)
@@ -88,7 +90,7 @@ fun LoadingRow(
} else if (showIfEmpty) {
LoadingRowPlaceholder(
title = title,
- message = "No results",
+ message = stringResource(R.string.no_results),
modifier = modifier,
)
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt
index ec193b2f..3c7e438a 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt
@@ -8,6 +8,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
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.main.HomePageContent
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.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@@ -113,11 +113,11 @@ class RecommendedMovieViewModel
rows.forEach { it.init() }
val homeRows =
listOf(
- HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"),
- HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"),
- HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"),
- HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"),
- HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"),
+ HomeRow(R.string.continue_watching, resumeItems),
+ HomeRow(R.string.recently_released, recentlyReleasedItems),
+ HomeRow(R.string.recently_added, recentlyAddedItems),
+ HomeRow(R.string.suggestions, suggestedItems),
+ HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
).filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) {
this@RecommendedMovieViewModel.rows.value = homeRows
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt
index 589403f2..d6f733a9 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt
@@ -8,6 +8,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
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.main.HomePageContent
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.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@@ -124,12 +124,12 @@ class RecommendedTvShowViewModel
rows.forEach { it.init() }
val homeRows =
listOf(
- HomeRow(HomeSection.RESUME, resumeItems, "Continue Watching"),
- HomeRow(HomeSection.NEXT_UP, nextUpItems, "Next Up"),
- HomeRow(HomeSection.LATEST_MEDIA, recentlyReleasedItems, "Recently Released"),
- HomeRow(HomeSection.LATEST_MEDIA, recentlyAddedItems, "Recently Added"),
- HomeRow(HomeSection.NONE, suggestedItems, "Suggestions"),
- HomeRow(HomeSection.NONE, unwatchedTopRatedItems, "Top Rated Unwatched"),
+ HomeRow(R.string.continue_watching, resumeItems),
+ HomeRow(R.string.next_up, nextUpItems),
+ HomeRow(R.string.recently_released, recentlyReleasedItems),
+ HomeRow(R.string.recently_added, recentlyAddedItems),
+ HomeRow(R.string.suggestions, suggestedItems),
+ HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
).filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) {
this@RecommendedTvShowViewModel.rows.value = homeRows
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt
index 7be5878a..63b904b6 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt
@@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.ProvideTextStyle
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.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.letNotEmpty
@@ -74,24 +76,30 @@ fun MediaSourceInfo(
modifier = modifier,
) {
Text(
- text = "Name: ${source.name}",
+ text = stringResource(R.string.name) + ": ${source.name}",
)
Text(
text = "ID: ${source.id}",
)
if (showFilePath) {
Text(
- text = "Path: ${source.path}",
+ text = stringResource(R.string.path) + ": ${source.path}",
)
}
source.size?.let { size ->
Text(
- text = "Size: ${formatBytes(size)}",
+ text = stringResource(R.string.file_size) + ": ${formatBytes(size)}",
)
}
source.bitrate?.let { bitrate ->
Text(
- text = "Bitrate: ${formatBytes(bitrate, byteRateSuffixes)}",
+ text =
+ stringResource(R.string.bitrate) + ": ${
+ formatBytes(
+ bitrate,
+ byteRateSuffixes,
+ )
+ }",
)
}
source.mediaStreams?.letNotEmpty { streams ->
@@ -109,7 +117,7 @@ fun MediaSourceInfo(
stream.profile?.let(::add)
}
Text(
- text = "Video: " + data.joinToString(" - "),
+ text = stringResource(R.string.video) + ": " + data.joinToString(" - "),
)
}
@@ -121,10 +129,10 @@ fun MediaSourceInfo(
stream.channelLayout?.let(::add)
stream.bitRate?.let { add(formatBytes(it, byteRateSuffixes)) }
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 = "Audio ${index + 1}: " + data.joinToString(" - "),
+ text = stringResource(R.string.audio) + " ${index + 1}: " + data.joinToString(" - "),
)
}
@@ -133,12 +141,14 @@ fun MediaSourceInfo(
buildList {
stream.language?.let { add(languageName(it)) }
stream.codec?.let(::add)
- if (stream.isDefault) add("Default")
- if (stream.isForced) add("Forced")
- if (stream.isExternal) add("External")
+ if (stream.isDefault) add(stringResource(R.string.default_track))
+ if (stream.isForced) add(stringResource(R.string.forced_track))
+ if (stream.isExternal) add(stringResource(R.string.external_track))
}
Text(
- text = "Subtitle ${index + 1}: " + data.joinToString(" - "),
+ text =
+ stringResource(R.string.subtitle) + " ${index + 1}: " +
+ data.joinToString(" - "),
)
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt
index bea5d281..7fa770ba 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt
@@ -42,6 +42,7 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Button
@@ -298,7 +299,7 @@ fun CardGrid(
// focusedIndex = -1
Box(modifier = Modifier.fillMaxSize()) {
Text(
- text = "No results",
+ text = stringResource(R.string.no_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.Center),
)
@@ -326,6 +327,8 @@ fun CardGrid(
}
}
}
+ val context = LocalContext.current
+ val letters = context.getString(R.string.jump_letters)
// Letters
val currentLetter =
remember(focusedIndex) {
@@ -344,10 +347,11 @@ fun CardGrid(
null
}
}
- ?: LETTERS[0]
+ ?: letters[0]
}
if (showLetterButtons && pager.isNotEmpty()) {
AlphabetButtons(
+ letters = letters,
currentLetter = currentLetter,
modifier = Modifier.align(Alignment.CenterVertically),
letterClicked = { letter ->
@@ -404,18 +408,18 @@ fun JumpButton(
}
}
-private const val LETTERS = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-
@Composable
fun AlphabetButtons(
+ letters: String,
currentLetter: Char,
letterClicked: (Char) -> Unit,
modifier: Modifier = Modifier,
) {
+ val context = LocalContext.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
- val index = LETTERS.indexOf(currentLetter)
+ val index = letters.indexOf(currentLetter)
LaunchedEffect(currentLetter) {
scope.launch(ExceptionHandler()) {
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(
state = listState,
modifier =
modifier.focusProperties {
onEnter = {
- focusRequesters[index.coerceIn(0, LETTERS.length - 1)].tryRequestFocus()
+ focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus()
}
},
) {
items(
- LETTERS.length,
- key = { LETTERS[it] },
+ letters.length,
+ key = { letters[it] },
) { index ->
val interactionSource = remember { MutableInteractionSource() }
val focused by interactionSource.collectIsFocusedAsState()
@@ -452,17 +456,17 @@ fun AlphabetButtons(
contentPadding = PaddingValues(2.dp),
interactionSource = interactionSource,
onClick = {
- letterClicked.invoke(LETTERS[index])
+ letterClicked.invoke(letters[index])
},
) {
val color =
- if (!focused && LETTERS[index] == currentLetter) {
+ if (!focused && letters[index] == currentLetter) {
MaterialTheme.colorScheme.tertiary
} else {
LocalContentColor.current
}
Text(
- text = LETTERS[index].toString(),
+ text = letters[index].toString(),
color = color,
)
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt
index 368df5ee..1716a926 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
@@ -33,6 +34,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
@@ -96,8 +98,8 @@ fun CollectionFolderLiveTv(
val tabs =
listOf(
- TabId("Guide", UUID.randomUUID()),
- TabId("DVR Schedule", UUID.randomUUID()),
+ TabId(stringResource(R.string.tv_guide), UUID.randomUUID()),
+ TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()),
) + folders
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt
index 426d8b3b..c9cc3e59 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt
@@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
@@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
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.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
@@ -52,7 +54,13 @@ fun CollectionFolderMovie(
val rememberedTabIndex =
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 selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt
index 0e5728ad..bb659e96 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt
@@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
@@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
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.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
@@ -52,7 +54,12 @@ fun CollectionFolderTv(
val rememberedTabIndex =
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 selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt
index 2f7c0ede..c55b2a4e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt
@@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.detail
+import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.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
*/
fun buildMoreDialogItems(
+ context: Context,
item: BaseItem,
series: BaseItem?,
sourceId: UUID?,
@@ -50,7 +52,7 @@ fun buildMoreDialogItems(
buildList {
add(
DialogItem(
- "Play",
+ context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
@@ -83,7 +85,7 @@ fun buildMoreDialogItems(
series?.let {
add(
DialogItem(
- "Go to series",
+ context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward,
) {
navigateTo(
@@ -100,7 +102,10 @@ fun buildMoreDialogItems(
if (sources.size > 1) {
add(
DialogItem(
- "Choose Version",
+ context.getString(
+ R.string.choose_stream,
+ context.getString(R.string.version),
+ ),
R.string.fa_file_video,
) {
onChooseVersion.invoke()
@@ -116,7 +121,10 @@ fun buildMoreDialogItems(
if (audioCount > 1) {
add(
DialogItem(
- "Choose audio",
+ context.getString(
+ R.string.choose_stream,
+ context.getString(R.string.audio),
+ ),
R.string.fa_volume_low,
) {
onChooseTracks.invoke(MediaStreamType.AUDIO)
@@ -126,7 +134,10 @@ fun buildMoreDialogItems(
if (subtitleCount > 0) {
add(
DialogItem(
- "Choose subtitles",
+ context.getString(
+ R.string.choose_stream,
+ context.getString(R.string.subtitles),
+ ),
R.string.fa_closed_captioning,
) {
onChooseTracks.invoke(MediaStreamType.SUBTITLE)
@@ -138,6 +149,7 @@ fun buildMoreDialogItems(
}
fun buildMoreDialogItemsForHome(
+ context: Context,
item: BaseItem,
seriesId: UUID?,
playbackPosition: Duration,
@@ -151,7 +163,7 @@ fun buildMoreDialogItemsForHome(
val itemId = item.id
add(
DialogItem(
- "Go To",
+ context.getString(R.string.go_to),
Icons.Default.ArrowForward,
) {
navigateTo(item.destination())
@@ -161,7 +173,7 @@ fun buildMoreDialogItemsForHome(
if (playbackPosition >= 1.seconds) {
add(
DialogItem(
- "Resume",
+ context.getString(R.string.resume),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
@@ -175,7 +187,7 @@ fun buildMoreDialogItemsForHome(
)
add(
DialogItem(
- "Restart",
+ context.getString(R.string.restart),
Icons.Default.Refresh,
// iconColor = Color.Green.copy(alpha = .8f),
) {
@@ -190,7 +202,7 @@ fun buildMoreDialogItemsForHome(
} else {
add(
DialogItem(
- "Play",
+ context.getString(R.string.play),
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
@@ -224,7 +236,7 @@ fun buildMoreDialogItemsForHome(
seriesId?.let {
add(
DialogItem(
- "Go to series",
+ context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward,
) {
navigateTo(
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt
index 92fa57c9..7365452e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt
@@ -21,6 +21,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
@@ -29,6 +30,7 @@ import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
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.GetItemsFilter
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() } }
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt
index 86dfbdbd..aaab1b9c 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt
@@ -24,6 +24,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
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.surfaceColorAtElevation
import coil3.compose.AsyncImage
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
@@ -258,7 +260,7 @@ fun PersonPageContent(
}
item {
LoadingRow(
- title = rowTitle("Movies", movies),
+ title = rowTitle(stringResource(R.string.movies), movies),
state = movies,
rowIndex = MOVIE_ROW,
position = position,
@@ -271,7 +273,7 @@ fun PersonPageContent(
}
item {
LoadingRow(
- title = rowTitle("Series", series),
+ title = rowTitle(stringResource(R.string.tv_shows), series),
state = series,
rowIndex = SERIES_ROW,
position = position,
@@ -284,7 +286,7 @@ fun PersonPageContent(
}
item {
LoadingRow(
- title = rowTitle("Episodes", episodes),
+ title = rowTitle(stringResource(R.string.episodes), episodes),
state = episodes,
rowIndex = EPISODE_ROW,
position = position,
@@ -365,9 +367,14 @@ fun PersonHeader(
val age = if (deathdate == null) birthdate.until(LocalDate.now())?.years else null
val text =
if (age != null) {
- "Born: ${formatDate(it)} ($age years old)"
+ stringResource(R.string.born) + ": ${formatDate(it)} (${
+ stringResource(
+ R.string.years_old,
+ age,
+ )
+ })"
} else {
- "Born: ${formatDate(it)}"
+ stringResource(R.string.born) + ": ${formatDate(it)}"
}
Text(
text = text,
@@ -379,7 +386,7 @@ fun PersonHeader(
}
birthPlace?.let {
Text(
- text = "Birthplace: $it",
+ text = stringResource(R.string.birthplace) + ": $it",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
@@ -390,9 +397,14 @@ fun PersonHeader(
val age = birthdate?.until(it)?.years
val text =
if (age != null) {
- "Died: ${formatDate(it)} ($age years old)"
+ stringResource(R.string.died) + ": ${formatDate(it)} (${
+ stringResource(
+ R.string.years_old,
+ age,
+ )
+ })"
} else {
- "Died: ${formatDate(it)}"
+ stringResource(R.string.died) + ": ${formatDate(it)}"
}
Text(
text = text,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
index 49538cca..5ab05521 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt
@@ -42,6 +42,8 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -123,6 +125,7 @@ fun PlaylistDetails(
modifier: Modifier = Modifier,
viewModel: PlaylistViewModel = hiltViewModel(),
) {
+ val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.init(destination.itemId)
}
@@ -170,13 +173,13 @@ fun PlaylistDetails(
items =
listOf(
DialogItem(
- "Go to",
+ context.getString(R.string.go_to),
Icons.Default.ArrowForward,
) {
viewModel.navigationManager.navigateTo(item.destination())
},
DialogItem(
- "Play from here",
+ context.getString(R.string.play_from_here),
Icons.Default.PlayArrow,
) {
viewModel.navigationManager.navigateTo(
@@ -282,7 +285,7 @@ fun PlaylistDetailsContent(
.padding(horizontal = 16.dp),
) {
Text(
- text = playlist.name ?: "Playlist",
+ text = playlist.name ?: stringResource(R.string.playlist),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.displayMedium,
textAlign = TextAlign.Center,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt
index ad43c16f..45578f4b 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt
@@ -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.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -31,6 +32,7 @@ import androidx.tv.material3.ListItemDefaults
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.components.ErrorMessage
@@ -202,7 +204,7 @@ fun DvrScheduleContent(
if (activeRecordings.isEmpty() && scheduledRecordings.isEmpty()) {
item {
Text(
- text = "No scheduled recordings",
+ text = stringResource(R.string.no_scheduled_recordings),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
@@ -213,7 +215,7 @@ fun DvrScheduleContent(
if (activeRecordings.isNotEmpty()) {
item {
Text(
- text = "Active",
+ text = stringResource(R.string.active_recordings),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.fillMaxWidth(),
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt
index e26dd5da..d69e6eab 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt
@@ -1,9 +1,11 @@
package com.github.damontecres.wholphin.ui.detail.livetv
+import android.content.Context
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AppColors
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.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@@ -45,6 +48,7 @@ const val MAX_HOURS = 48L
class LiveTvViewModel
@Inject
constructor(
+ @param:ApplicationContext private val context: Context,
val api: ApiClient,
val navigationManager: NavigationManager,
) : ViewModel() {
@@ -169,7 +173,7 @@ class LiveTvViewModel
startHours = it.toFloat(),
endHours = (it + 1).toFloat(),
duration = 60.seconds,
- name = "No data",
+ name = context.getString(R.string.no_data),
subtitle = null,
seasonEpisode = null,
isRecording = false,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt
index a5f6db0b..933b20ef 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -30,6 +31,7 @@ import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ErrorMessage
@@ -165,7 +167,14 @@ fun ProgramDialog(
)
}
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 = 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(
imageVector = Icons.Default.PlayArrow,
- contentDescription = "Delete",
+ contentDescription = stringResource(R.string.delete),
tint = Color.Green.copy(alpha = .75f),
)
Text(
- text = "Watch live",
+ text = stringResource(R.string.watch_live),
)
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt
index 11ce4ba4..5322a9e1 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt
@@ -150,7 +150,7 @@ fun MovieDetails(
overviewOnClick = {
overviewDialog =
ItemDetailsDialogInfo(
- title = movie.name ?: "Unknown",
+ title = movie.name ?: context.getString(R.string.unknown),
overview = movie.data.overview,
files = movie.data.mediaSources.orEmpty(),
)
@@ -162,6 +162,7 @@ fun MovieDetails(
title = movie.name + " (${movie.data.productionYear ?: ""})",
items =
buildMoreDialogItems(
+ context = context,
item = movie,
watched = movie.data.userData?.played ?: false,
favorite = movie.data.userData?.isFavorite ?: false,
@@ -172,7 +173,10 @@ fun MovieDetails(
onClickFavorite = viewModel::setFavorite,
onChooseVersion = {
chooseVersion =
- chooseVersionParams(movie.data.mediaSources!!) { idx ->
+ chooseVersionParams(
+ context,
+ movie.data.mediaSources!!,
+ ) { idx ->
val source = movie.data.mediaSources!![idx]
viewModel.savePlayVersion(
movie,
@@ -188,6 +192,7 @@ fun MovieDetails(
)?.let { source ->
chooseVersion =
chooseStream(
+ context = context,
streams = source.mediaStreams.orEmpty(),
type = type,
onClick = { trackIndex ->
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt
index 74faaccf..534fe23e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt
@@ -147,7 +147,7 @@ fun MovieDetailsHeader(
?.joinToString(", ") { it.name!! }
?.let {
Text(
- text = "Directed by $it",
+ text = stringResource(R.string.directed_by, it),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -183,7 +183,7 @@ fun MovieDetailsHeader(
?.let {
if (it.isNotNullOrBlank()) {
TitleValueText(
- "Subtitles",
+ stringResource(R.string.subtitles),
it,
modifier = Modifier.widthIn(max = 200.dp),
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
index a27507ca..7284ba51 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
@@ -1,7 +1,9 @@
package com.github.damontecres.wholphin.ui.detail.movie
+import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
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.ThemeSongPlayer
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -43,6 +46,7 @@ class MovieViewModel
@Inject
constructor(
api: ApiClient,
+ @param:ApplicationContext private val context: Context,
private val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
@@ -79,7 +83,7 @@ class MovieViewModel
// TODO would be nice to clean up the trailer name
// ?.replace(item.name ?: "", "")
// ?.removePrefix(" - ")
- ?: "Trailer"
+ ?: context.getString(R.string.trailer)
RemoteTrailer(name, url)
}
}.orEmpty()
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt
index 927a08e8..ec4c4b6f 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt
@@ -82,7 +82,7 @@ fun FocusedEpisodeFooter(
?.let {
if (it.isNotNullOrBlank()) {
TitleValueText(
- "Subtitles",
+ stringResource(R.string.subtitles),
it,
modifier = Modifier.widthIn(max = 160.dp),
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt
index fb2f7a04..a71d9423 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt
@@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.detail.series
+import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -83,6 +85,7 @@ fun SeriesDetails(
modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel(),
) {
+ val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.init(preferences, destination.itemId, destination.item, null)
}
@@ -136,7 +139,8 @@ fun SeriesDetails(
onLongClickItem = { season ->
seasonDialog =
buildDialogForSeason(
- season,
+ context = context,
+ s = season,
onClickItem = { viewModel.navigateTo(it.destination()) },
markPlayed = { played ->
viewModel.setSeasonWatched(season.id, played)
@@ -146,7 +150,7 @@ fun SeriesDetails(
overviewOnClick = {
overviewDialog =
ItemDetailsDialogInfo(
- title = item.name ?: "Unknown",
+ title = item.name ?: context.getString(R.string.unknown),
overview = item.data.overview,
files = listOf(),
)
@@ -161,7 +165,8 @@ fun SeriesDetails(
if (showWatchConfirmation) {
ConfirmDialog(
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 = {
showWatchConfirmation = false
},
@@ -343,7 +348,7 @@ fun SeriesDetailsContent(
}
item {
ItemRow(
- title = "Seasons",
+ title = stringResource(R.string.tv_seasons),
items = seasons,
onClickItem = {
position = SEASONS_ROW
@@ -444,7 +449,7 @@ fun SeriesDetailsHeader(
modifier = modifier,
) {
Text(
- text = series.name ?: "Unknown",
+ text = series.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.displaySmall,
modifier = Modifier.fillMaxWidth(),
)
@@ -487,6 +492,7 @@ fun SeriesDetailsHeader(
}
fun buildDialogForSeason(
+ context: Context,
s: BaseItem,
onClickItem: (BaseItem) -> Unit,
markPlayed: (Boolean) -> Unit,
@@ -494,26 +500,26 @@ fun buildDialogForSeason(
val items =
buildList {
add(
- DialogItem("Go to", Icons.Default.PlayArrow) {
+ DialogItem(context.getString(R.string.go_to), Icons.Default.PlayArrow) {
onClickItem.invoke(s)
},
)
if (s.data.userData?.played == true) {
add(
- DialogItem("Mark as unplayed", R.string.fa_eye) {
+ DialogItem(context.getString(R.string.mark_unwatched), R.string.fa_eye) {
markPlayed.invoke(false)
},
)
} else {
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)
},
)
}
}
return DialogParams(
- title = s.name ?: "Season",
+ title = s.name ?: context.getString(R.string.tv_season),
fromLongClick = true,
items = items,
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt
index 75ea4555..ef0a0eaf 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt
@@ -13,8 +13,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.platform.LocalContext
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
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.chooseSource
import com.github.damontecres.wholphin.preferences.UserPreferences
@@ -72,6 +74,7 @@ fun SeriesOverview(
viewModel: SeriesViewModel = hiltViewModel(),
initialSeasonEpisode: SeasonEpisodeIds? = null,
) {
+ val context = LocalContext.current
val firstItemFocusRequester = remember { FocusRequester() }
val episodeRowFocusRequester = remember { FocusRequester() }
@@ -178,7 +181,8 @@ fun SeriesOverview(
title = series.name + " - " + ep.data.seasonEpisode,
items =
buildMoreDialogItems(
- ep,
+ context = context,
+ item = ep,
watched = ep.data.userData?.played ?: false,
favorite = ep.data.userData?.isFavorite ?: false,
series = series,
@@ -208,7 +212,10 @@ fun SeriesOverview(
},
onChooseVersion = {
chooseVersion =
- chooseVersionParams(ep.data.mediaSources!!) { idx ->
+ chooseVersionParams(
+ context,
+ ep.data.mediaSources!!,
+ ) { idx ->
val source = ep.data.mediaSources!![idx]
viewModel.savePlayVersion(
ep,
@@ -224,6 +231,7 @@ fun SeriesOverview(
)?.let { source ->
chooseVersion =
chooseStream(
+ context = context,
streams = source.mediaStreams.orEmpty(),
type = type,
onClick = { trackIndex ->
@@ -313,7 +321,7 @@ fun SeriesOverview(
episodeList?.getOrNull(position.episodeRowIndex)?.let {
overviewDialog =
ItemDetailsDialogInfo(
- title = it.name ?: "Unknown",
+ title = it.name ?: context.getString(R.string.unknown),
overview = it.data.overview,
files = it.data.mediaSources.orEmpty(),
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt
index db9402cf..0629ce6d 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt
@@ -33,6 +33,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
@@ -42,6 +43,7 @@ import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
@@ -178,7 +180,9 @@ fun SeriesOverviewContent(
.focusRequester(focusRequesters[index]),
) {
Text(
- text = season.name ?: "Season ${season.data.indexNumber}",
+ text =
+ season.name
+ ?: (stringResource(R.string.tv_season) + " ${season.data.indexNumber}"),
modifier = Modifier.padding(8.dp),
)
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt
index 2e116b5d..74f2849c 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt
@@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.ui.main
import android.widget.Toast
+import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -71,10 +72,20 @@ import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
data class HomeRow(
- val section: HomeSection,
+ @param:StringRes val titleRes: Int?,
+ val title: String?,
val items: List,
- val title: String? = null,
-)
+) {
+ constructor(
+ @StringRes titleRes: Int,
+ items: List,
+ ) : this(titleRes, null, items)
+
+ constructor(
+ title: String,
+ items: List,
+ ) : this(null, title, items)
+}
@Composable
fun HomePage(
@@ -121,6 +132,7 @@ fun HomePage(
onLongClickItem = {
val dialogItems =
buildMoreDialogItemsForHome(
+ context = context,
item = it,
seriesId = it.data.seriesId,
playbackPosition =
@@ -242,7 +254,7 @@ fun HomePageContent(
itemsIndexed(homeRows) { rowIndex, row ->
if (row.items.isNotEmpty()) {
ItemRow(
- title = row.title ?: stringResource(row.section.nameRes),
+ title = row.title ?: row.titleRes?.let { stringResource(it) } ?: "",
items = row.items,
onClickItem = onClickItem,
cardOnFocus = { isFocused, index ->
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeSection.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeSection.kt
deleted file mode 100644
index 60978114..00000000
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeSection.kt
+++ /dev/null
@@ -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
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt
index 3db3f630..806ab9f0 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt
@@ -1,8 +1,10 @@
package com.github.damontecres.wholphin.ui.main
+import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
import com.github.damontecres.wholphin.data.ServerRepository
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.supportItemKinds
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -40,6 +43,7 @@ import javax.inject.Inject
class HomeViewModel
@Inject
constructor(
+ @param:ApplicationContext private val context: Context,
val api: ApiClient,
val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
@@ -86,7 +90,7 @@ class HomeViewModel
if (resume.isNotEmpty()) {
add(
HomeRow(
- section = HomeSection.RESUME,
+ titleRes = R.string.recently_added,
items = resume,
),
)
@@ -94,7 +98,7 @@ class HomeViewModel
if (nextUp.isNotEmpty()) {
add(
HomeRow(
- section = HomeSection.NEXT_UP,
+ titleRes = R.string.next_up,
items = nextUp,
),
)
@@ -191,10 +195,10 @@ class HomeViewModel
.mapNotNull { view ->
val title =
if (view.collectionType == CollectionType.LIVETV) {
- "Recently Recorded"
+ context.getString(R.string.recently_recorded)
} 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 =
if (view.collectionType == CollectionType.LIVETV) {
api.liveTvApi
@@ -222,9 +226,8 @@ class HomeViewModel
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
- section = HomeSection.LATEST_MEDIA,
- items = latest,
title = title,
+ items = latest,
)
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt
index 341f8c86..6d464fa7 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt
@@ -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.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
@@ -28,6 +30,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.Cards
@@ -153,6 +156,7 @@ fun SearchPage(
modifier: Modifier = Modifier,
viewModel: SearchViewModel = hiltViewModel(),
) {
+ val context = LocalContext.current
val movies by viewModel.movies.observeAsState(SearchResult.NoQuery)
val collections by viewModel.collections.observeAsState(SearchResult.NoQuery)
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
@@ -198,7 +202,7 @@ fun SearchPage(
}
}
searchResultRow(
- title = "Movies",
+ title = context.getString(R.string.movies),
result = movies,
rowIndex = MOVIE_ROW,
position = position,
@@ -208,7 +212,7 @@ fun SearchPage(
modifier = Modifier.fillMaxWidth(),
)
searchResultRow(
- title = "Collections",
+ title = context.getString(R.string.collections),
result = collections,
rowIndex = COLLECTION_ROW,
position = position,
@@ -218,7 +222,7 @@ fun SearchPage(
modifier = Modifier.fillMaxWidth(),
)
searchResultRow(
- title = "Series",
+ title = context.getString(R.string.tv_shows),
result = series,
rowIndex = SERIES_ROW,
position = position,
@@ -228,7 +232,7 @@ fun SearchPage(
modifier = Modifier.fillMaxWidth(),
)
searchResultRow(
- title = "Episodes",
+ title = context.getString(R.string.episodes),
result = episodes,
rowIndex = EPISODE_ROW,
position = position,
@@ -308,7 +312,7 @@ fun LazyListScope.searchResultRow(
SearchResult.Searching ->
SearchResultPlaceholder(
title = title,
- message = "Searching...",
+ message = stringResource(R.string.searching),
modifier = modifier,
)
@@ -316,7 +320,7 @@ fun LazyListScope.searchResultRow(
if (r.items.isEmpty()) {
SearchResultPlaceholder(
title = title,
- message = "No results",
+ message = stringResource(R.string.no_results),
modifier = modifier,
)
} else {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt
index bc18b47c..de463e99 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt
@@ -324,7 +324,7 @@ fun NavDrawer(
val focused by interactionSource.collectIsFocusedAsState()
LaunchedEffect(focused) { if (focused) focusedIndex = -2 }
IconNavItem(
- text = "Search",
+ text = stringResource(R.string.search),
icon = Icons.Default.Search,
selected = selectedIndex == -2,
interactionSource = interactionSource,
@@ -345,7 +345,7 @@ fun NavDrawer(
val focused by interactionSource.collectIsFocusedAsState()
LaunchedEffect(focused) { if (focused) focusedIndex = -1 }
IconNavItem(
- text = "Home",
+ text = stringResource(R.string.home),
icon = Icons.Default.Home,
selected = selectedIndex == -1,
interactionSource = interactionSource,
@@ -413,7 +413,7 @@ fun NavDrawer(
val focused by interactionSource.collectIsFocusedAsState()
LaunchedEffect(focused) { if (focused) focusedIndex = Int.MIN_VALUE }
IconNavItem(
- text = "Settings",
+ text = stringResource(R.string.settings),
icon = Icons.Default.Settings,
selected = false,
interactionSource = interactionSource,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt
index ab19bbf6..4842f740 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt
@@ -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.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.unit.dp
import androidx.tv.material3.Icon
@@ -35,6 +37,7 @@ import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogItemDivider
@@ -58,7 +61,7 @@ fun DownloadSubtitlesContent(
SubtitleSearch.Searching -> {
Wrapper {
Text(
- text = "Searching...",
+ text = stringResource(R.string.searching),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -68,7 +71,7 @@ fun DownloadSubtitlesContent(
SubtitleSearch.Downloading -> {
Wrapper {
Text(
- text = "Downloading...",
+ text = stringResource(R.string.downloading),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -199,9 +202,15 @@ fun convertRemoteSubtitles(
op.providerName?.let(::add)
op.threeLetterIsoLanguageName?.let(::add)
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 = strings.joinToString(" - "),
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/Models.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/Models.kt
index 71374ffc..2811ecc3 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/Models.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/Models.kt
@@ -1,5 +1,8 @@
package com.github.damontecres.wholphin.ui.playback
+import com.github.damontecres.wholphin.R
+import com.github.damontecres.wholphin.WholphinApplication
+
data class SubtitleStream(
val index: Int,
val language: String?,
@@ -17,7 +20,8 @@ data class SubtitleStream(
language,
title,
codec,
- ).joinToString(" - ").ifBlank { "Unknown" }
+ ).joinToString(" - ")
+ .ifBlank { WholphinApplication.instance.getString(R.string.unknown) }
}
data class AudioStream(
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt
index dc7036da..fc39255e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt
@@ -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.layout.ContentScale
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -34,6 +35,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@@ -68,7 +70,7 @@ fun NextUpEpisode(
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Text(
- text = "Up Next...",
+ text = stringResource(R.string.next_up) + "...",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt
index 60313567..5df69f4b 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt
@@ -46,6 +46,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
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.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
+import com.github.damontecres.wholphin.util.stringRes
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -212,7 +214,7 @@ fun PlaybackControls(
.padding(end = 32.dp),
) {
Text(
- text = "Skip ${segment.type.serialName}",
+ text = stringResource(R.string.skip) + " " + stringResource(segment.type.stringRes),
)
}
}
@@ -332,7 +334,7 @@ fun LeftPlaybackButtons(
val options =
buildList {
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(
choices = options,
@@ -407,7 +409,12 @@ fun RightPlaybackButtons(
val currentChoice =
subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex } ?: subtitleStreams.size
BottomDialog(
- choices = options + listOf("None", "Search & Download"),
+ choices =
+ options +
+ listOf(
+ stringResource(R.string.none),
+ stringResource(R.string.search_and_download),
+ ),
currentChoice = currentChoice,
onDismissRequest = {
onControllerInteraction.invoke()
@@ -434,7 +441,12 @@ fun RightPlaybackButtons(
)
}
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(
choices = options,
currentChoice = null,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt
index d8d338da..912cb214 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt
@@ -38,11 +38,13 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.Player
import androidx.tv.material3.MaterialTheme
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.Chapter
import com.github.damontecres.wholphin.data.model.ItemPlayback
@@ -199,7 +201,7 @@ fun PlaybackOverlay(
},
) {
Text(
- text = "Chapters",
+ text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge,
)
LazyRow(
@@ -240,7 +242,7 @@ fun PlaybackOverlay(
}
if (playlist.hasNext()) {
Text(
- text = "Queue",
+ text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
@@ -282,7 +284,7 @@ fun PlaybackOverlay(
},
) {
Text(
- text = "Queue",
+ text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge,
)
LazyRow(
@@ -489,7 +491,7 @@ fun Controller(
when (nextState) {
OverlayViewState.CHAPTERS ->
Text(
- text = "Chapters",
+ text = stringResource(R.string.chapters),
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
@@ -501,7 +503,7 @@ fun Controller(
OverlayViewState.QUEUE ->
Text(
- text = "Queue",
+ text = stringResource(R.string.queue),
style = MaterialTheme.typography.titleLarge,
modifier =
Modifier
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt
index ec9dc464..caf4f1ee 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt
@@ -39,6 +39,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@@ -60,6 +61,7 @@ import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
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.Playlist
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.LoadingState
import com.github.damontecres.wholphin.util.seasonEpisode
+import com.github.damontecres.wholphin.util.stringRes
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.DeviceProfile
@@ -411,7 +414,7 @@ fun PlaybackPage(
modifier = Modifier.focusRequester(focusRequester),
) {
Text(
- text = "Skip ${segment.type.serialName}",
+ text = stringResource(R.string.skip) + " " + stringResource(segment.type.stringRes),
)
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
index d1b523a6..5ed4e19b 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
@@ -19,6 +19,7 @@ import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository
@@ -963,7 +964,7 @@ class PlaybackViewModel
if (maxAttempts == 0) {
showToast(
context,
- "Download is taking a long time, you may need to restart playback",
+ context.getString(R.string.subtitle_download_too_long),
)
} else {
// Find the new subtitle stream
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt
index 62b21a97..2844207b 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt
@@ -97,9 +97,9 @@ fun PreferencesContent(
}
val screenTitle =
when (preferenceScreenOption) {
- PreferenceScreenOption.BASIC -> "Preferences"
- PreferenceScreenOption.ADVANCED -> "Advanced Preferences"
- PreferenceScreenOption.USER_INTERFACE -> "User Interface Preferences"
+ PreferenceScreenOption.BASIC -> R.string.settings
+ PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
+ PreferenceScreenOption.USER_INTERFACE -> R.string.ui_interface
}
var visible by remember { mutableStateOf(false) }
@@ -126,7 +126,7 @@ fun PreferencesContent(
) {
stickyHeader {
Text(
- text = screenTitle,
+ text = stringResource(screenTitle),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt
index c9ff33aa..edb461f2 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt
@@ -33,6 +33,7 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
@@ -42,6 +43,7 @@ import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
+import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.components.ErrorMessage
@@ -221,7 +223,7 @@ fun InstallUpdatePageContent(
).padding(16.dp),
) {
Text(
- text = "Update available",
+ text = stringResource(R.string.update_available),
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -234,14 +236,14 @@ fun InstallUpdatePageContent(
onClick = onInstallRelease,
) {
Text(
- text = "Download & Update",
+ text = stringResource(R.string.download_and_update),
)
}
Button(
onClick = onCancel,
) {
Text(
- text = "Cancel",
+ text = stringResource(R.string.cancel),
)
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt
index 8dadb4fb..a83cfb6d 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt
@@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Icon
@@ -96,7 +97,7 @@ fun ServerList(
ListItem(
enabled = true,
selected = false,
- headlineContent = { Text(text = "Add Server") },
+ headlineContent = { Text(text = stringResource(R.string.add_server)) },
leadingContent = {
Icon(
imageVector = Icons.Default.Add,
@@ -116,11 +117,14 @@ fun ServerList(
title = server.name ?: server.url,
dialogItems =
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)
},
DialogItem(
- "Delete",
+ stringResource(R.string.delete),
Icons.Default.Delete,
Color.Red.copy(alpha = .8f),
) {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt
index e840a3e5..0dbce500 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment
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.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
@@ -31,6 +32,7 @@ import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
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.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox
@@ -78,7 +80,7 @@ fun SwitchServerContent(
),
) {
Text(
- text = "Select Server",
+ text = stringResource(R.string.select_server),
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -120,13 +122,13 @@ fun SwitchServerContent(
),
) {
Text(
- text = "Discovered Servers",
+ text = stringResource(R.string.discovered_servers),
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
if (discoveredServers.isEmpty()) {
Text(
- text = "Searching...",
+ text = stringResource(R.string.searching),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -184,7 +186,7 @@ fun SwitchServerContent(
.fillMaxWidth(.4f),
) {
Text(
- text = "Enter Server URL",
+ text = stringResource(R.string.enter_server_url),
)
EditTextBox(
value = url,
@@ -227,7 +229,7 @@ fun SwitchServerContent(
if (state == LoadingState.Loading) {
CircularProgress(Modifier.size(32.dp))
} else {
- Text(text = "Submit")
+ Text(text = stringResource(R.string.submit))
}
}
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt
index b8d3c9d3..49e9be8a 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt
@@ -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.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
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.Text
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.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox
@@ -94,7 +96,7 @@ fun SwitchUserContent(
),
) {
Text(
- text = "Select User",
+ text = stringResource(R.string.select_user),
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt
index 37bc69c0..c4fb035e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt
@@ -67,7 +67,7 @@ fun UserList(
ListItem(
enabled = true,
selected = false,
- headlineContent = { Text(text = "Add User") },
+ headlineContent = { Text(text = stringResource(R.string.add_user)) },
leadingContent = {
Icon(
imageVector = Icons.Default.Add,
@@ -84,7 +84,7 @@ fun UserList(
ListItem(
enabled = true,
selected = false,
- headlineContent = { Text(text = "Switch servers") },
+ headlineContent = { Text(text = stringResource(R.string.switch_servers)) },
leadingContent = {
Text(
text = stringResource(R.string.fa_arrow_left_arrow_right),
@@ -102,11 +102,14 @@ fun UserList(
title = user.name ?: user.id.toString(),
dialogItems =
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)
},
DialogItem(
- "Delete",
+ stringResource(R.string.delete),
Icons.Default.Delete,
Color.Red.copy(alpha = .8f),
) {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt
index c6ec4bb5..e447758f 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt
@@ -1,16 +1,8 @@
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.CollectionType
-val supportedHomeSection =
- setOf(
- HomeSection.LATEST_MEDIA,
- HomeSection.NEXT_UP,
- HomeSection.RESUME,
- )
-
val supportItemKinds =
setOf(
BaseItemKind.MOVIE,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt
index 3c411724..0afdca90 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt
@@ -48,7 +48,7 @@ class CrashReportSender : ReportSender {
val api =
createJellyfin {
this.context = context
- clientInfo = AppModule.clientInfo()
+ clientInfo = AppModule.clientInfo(context)
deviceInfo = AppModule.deviceInfo(context)
apiClientFactory = okHttpFactory
socketConnectionFactory = okHttpFactory
diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt
index 7076276a..94595b0a 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt
@@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.util
import android.os.Build
+import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
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.ui.isNotNullOrBlank
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.MediaStreamType
import java.time.LocalDate
@@ -164,3 +166,15 @@ fun formatBytes(
}
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
+ }
diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml
index 6e38778a..1f7ba4d4 100644
--- a/app/src/main/res/values/preferences.xml
+++ b/app/src/main/res/values/preferences.xml
@@ -1,5 +1,53 @@
+ preference.update.threshold
+ preference.update.lastTimestamp
+
+ Device supports AC3/Dolby Digital
+ Advanced Settings
+ Advanced UI
+ Application Theme
+ Automatically check for updates
+ Delay before playing next up
+ Auto play next up
+ Check for updates
+ Applies to TV Series only
+
+ Direct play ASS subtitles
+ Direct play PGS subtitles
+ Always downmix to stereo
+ Use FFmpeg decoder module
+ Default content scale
+ Install update
+ Installed version
+ Max items on home page rows
+ Choose the default items to show, others will be hidden
+ Customize Navigation Drawer Items
+ Click to switch pages
+ Switch nav drawer pages on focus
+ Passout Protection
+ Play theme music
+ Playback overrides
+ Remember selected tabs
+ Enable rewatching in next up
+ Seek bar steps
+ Useful for debugging
+ Send app logs to current server
+ Will try to send to last connected server
+ Send Crash Reports
+ Settings
+ Skip back when resuming playback
+ Skip back
+ Skip commercials behavior
+ Skip forward
+ Skip intro behavior
+ Skip outro behavior
+ Skip previews behavior
+ Skip recap behavior
+ Update available
+ URL used to check for app updates
+ Update URL
+
- Disabled
- Lowest
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 67fd8735..7e6ac782 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,109 +1,157 @@
- Wholphin
+ Wholphin
+ Wholphin
- Recently added media
- My media
- My media (small)
- Continue watching
- Continue listening
- Continue reading
- Active recordings
- Next up
- Live TV
- None
- Search
- Resume
- Restart
- Play
- More
+ About
+ Active Recordings
+ Favorite
+ Add Server
+ Add User
+ Audio
+ Birthplace
+ Bitrate
+ Born
+ Cancel Recording
+ Cancel Series Recording
+ Cancel
+ Chapters
+ Choose %1$s
+ Clear image cache
+ Collection
+ Collections
+ Commercial
+ Community Rating
+ An error occurred! Press the button to send logs to your server.
+ Confirm
+ Continue watching
+ Critic Rating
+ %.1f seconds
+ Default
+ Delete
+ Died
+ Directed by %1$s
+ Director
+ Disabled
+ Discovered Servers
+
+ Downloading…
+ Enabled
+ Enter Server IP or URL including port
+ Episodes
+ Error loading collection %1$s
+ External
+ Favorites
+ Size
+ Forced
+ Genres
+ Go to series
+ Go To
+ Hide playback controls
+ Hide debug info
+ Hide
+ Home
+ Immediate
+ Intro
+ #ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ Library
+ License information
+ Live TV
+ Loading…
+ Mark entire series as played?
+ Mark entire series as unplayed?
Mark as unwatched
Mark as watched
- Cancel
- Save
- Wholphin
- Interface
- Playback
- About
- Advanced Settings
- Advanced UI
- Install update
- Installed version
- Show
- Hide
- Enabled
- Disabled
- Skip forward
- Skip back
- Hide playback controls
- Seek bar steps
- Max items on home page rows
- Enable rewatching in next up
- Studios
- Video
- Audio
- Genres
- Play theme music
- License information
+ Max bitrate
+ More like this
+ More
+ Movies
+ Name
+ Next Up
+ No data
+ No results
+ No scheduled recordings
+ No update available
+ None
+ Outro
+ Path
+ People
+ Play Count
+ Play from here
+ Play
Show playback debug info
- Delay before playing next up
- Auto play next up
-
-
- Name
- Random
- Date Episode Added
+ Playback Speed
+ Playback
+ Playlist
+ Preview
+ User Profile Settings
+ Queue
+ Recap
+ Recently added in %1$s
+ Recently added
+ Recently Recorded
+ Recently Released
+ Recommended
+ Record Program
+ Record Series
+ Unfavorite
+ Restart
+ Resume
+ Save
+
+ Search
+ Searching…
+ Select Server
+ Select User
+ Show debug info
+ Show next up
+ Show
+ Shuffle
+ Skip
Date Added
+ Date Episode Added
Date Played
Date Released
- Skip back when resuming playback
- preference.update.threshold
- preference.update.lastTimestamp
- Confirm
- Remember selected tabs
- Director
- Max bitrate
- Application Theme
- Skip recap behavior
- Skip previews behavior
- Skip commercials behavior
- Skip outro behavior
- Skip intro behavior
- Check for updates
- Automatically check for updates
- Update URL
- URL used to check for app updates
- Updates
- No update available
- Clear image cache
- Shuffle
-
- Device supports AC3/Dolby Digital
- Always downmix to stereo
- Playback overrides
- Direct play ASS subtitles
- Direct play PGS subtitles
+ Name
+ Random
+ Studios
+ Submit
+ Download is taking a long time, you may need to restart playback
+ Subtitle
+ Subtitles
+ Suggestions
+ Switch servers
+ Switch
+ Top Rated Unwatched
+ Trailer
Trailers
- Passout Protection
- More like this
- Unfavorite
- Favorite
- Favorites
- Customize Navigation Drawer Items
- User Profile Settings
- Choose the default items to show, others will be hidden
- Applies to TV Series only
- Send Crash Reports
- Will try to send to last connected server
- Send app logs to current server
- Useful for debugging
- Default content scale
- Use FFmpeg decoder module
- Switch nav drawer pages on focus
- Click to switch pages
- Show next up
- Next Up
- Community Rating
- Critic Rating
- Play Count
+ DVR Schedule
+ Guide
+ Season
+ Seasons
+ TV Shows
+ Interface
+ Unknown
+ Updates
+ Version
+ Video Scale
+ Video
+ Watch live
+ %1$d years old
+
+
+ - %d downloads
+ - %d download
+ - %d downloads
+
+
+ - %d hours
+ - %d hour
+ - %d hours
+
+
+ - %d seconds
+ - %d second
+ - %d seconds
+