Add settings to show the current time (#155)

Closes #154 

Adds a settings to show the current time in the upper right corner.

If enabled, it will also show the end time for the playback on the
overlay.
This commit is contained in:
damontecres 2025-11-04 15:27:38 -05:00 committed by GitHub
parent 40a8249469
commit 77c787b51f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 118 additions and 15 deletions

View file

@ -642,6 +642,18 @@ sealed interface AppPreference<T> {
indexToValue = { ShowNextUpWhen.forNumber(it) }, indexToValue = { ShowNextUpWhen.forNumber(it) },
valueToIndex = { it.number }, valueToIndex = { it.number },
) )
val ShowClock =
AppSwitchPreference(
title = R.string.show_clock,
defaultValue = true,
getter = { it.interfacePreferences.showClock },
setter = { prefs, value ->
prefs.updateInterfacePreferences { showClock = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
} }
} }
@ -709,6 +721,7 @@ val advancedPreferences =
title = R.string.ui_interface, title = R.string.ui_interface,
preferences = preferences =
listOf( listOf(
AppPreference.ShowClock,
AppPreference.CombineContinueNext, AppPreference.CombineContinueNext,
// Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418 // Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
// AppPreference.NavDrawerSwitchOnFocus, // AppPreference.NavDrawerSwitchOnFocus,

View file

@ -73,6 +73,7 @@ class AppPreferencesSerializer
appThemeColors = AppPreference.ThemeColors.defaultValue appThemeColors = AppPreference.ThemeColors.defaultValue
navDrawerSwitchOnFocus = navDrawerSwitchOnFocus =
AppPreference.NavDrawerSwitchOnFocus.defaultValue AppPreference.NavDrawerSwitchOnFocus.defaultValue
showClock = AppPreference.ShowClock.defaultValue
}.build() }.build()
}.build() }.build()

View file

@ -158,6 +158,7 @@ fun RecommendedMovie(
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = {}, onLongClickItem = {},
onFocusPosition = onFocusPosition, onFocusPosition = onFocusPosition,
showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -170,6 +170,7 @@ fun RecommendedTvShow(
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = {}, onLongClickItem = {},
onFocusPosition = onFocusPosition, onFocusPosition = onFocusPosition,
showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -132,6 +132,12 @@ fun SeriesOverviewContent(
modifier = Modifier, modifier = Modifier,
) { ) {
item { item {
val paddingValues =
if (preferences.appPreferences.interfacePreferences.showClock) {
PaddingValues(start = 16.dp, end = 100.dp)
} else {
PaddingValues(start = 16.dp, end = 16.dp)
}
TabRow( TabRow(
selectedTabIndex = focusTabIndex, selectedTabIndex = focusTabIndex,
modifier = modifier =
@ -140,7 +146,7 @@ fun SeriesOverviewContent(
focusRequesters.size > selectedTabIndex, focusRequesters.size > selectedTabIndex,
{ Modifier.focusRestorer(focusRequesters[selectedTabIndex]) }, { Modifier.focusRestorer(focusRequesters[selectedTabIndex]) },
).focusRequester(tabRowFocusRequester) ).focusRequester(tabRowFocusRequester)
.padding(horizontal = 16.dp) .padding(paddingValues)
.fillMaxWidth() .fillMaxWidth()
.onFocusChanged { .onFocusChanged {
if (!it.isFocused) { if (!it.isFocused) {

View file

@ -171,6 +171,7 @@ fun HomePage(
) )
}, },
loadingState = loading, loadingState = loading,
showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier, modifier = modifier,
) )
dialog?.let { params -> dialog?.let { params ->
@ -205,6 +206,7 @@ fun HomePageContent(
homeRows: List<HomeRow>, homeRows: List<HomeRow>,
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
onLongClickItem: (BaseItem) -> Unit, onLongClickItem: (BaseItem) -> Unit,
showClock: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null, onFocusPosition: ((RowColumn) -> Unit)? = null,
loadingState: LoadingState? = null, loadingState: LoadingState? = null,
@ -342,7 +344,7 @@ fun HomePageContent(
Box( Box(
modifier = modifier =
Modifier Modifier
.padding(16.dp) .padding(if (showClock) 40.dp else 20.dp)
.size(40.dp) .size(40.dp)
.align(Alignment.TopEnd), .align(Alignment.TopEnd),
) { ) {

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -27,6 +28,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -74,13 +76,16 @@ import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.spacedByWithFooter import com.github.damontecres.wholphin.ui.spacedByWithFooter
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.TimeFormatter
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber import timber.log.Timber
import java.time.LocalTime
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -431,13 +436,35 @@ fun NavDrawer(
} }
}, },
) { ) {
// Drawer content Box(
DestinationContent(
destination = destination,
preferences = preferences,
deviceProfile = deviceProfile,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) ) {
// Drawer content
DestinationContent(
destination = destination,
preferences = preferences,
deviceProfile = deviceProfile,
modifier = Modifier.fillMaxSize(),
)
if (preferences.appPreferences.interfacePreferences.showClock) {
var now by remember { mutableStateOf(LocalTime.now()) }
LaunchedEffect(Unit) {
if (isActive) {
now = LocalTime.now()
}
}
Text(
text = TimeFormatter.format(now),
// style = MaterialTheme.typography.titleMedium,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier =
Modifier
.align(Alignment.TopEnd)
.padding(vertical = 16.dp, horizontal = 24.dp),
)
}
}
} }
} }

View file

@ -282,6 +282,7 @@ fun SeekBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
val remaining = ((player.duration - position) / 1000).seconds
Text( Text(
text = (position / 1000).seconds.toString(), text = (position / 1000).seconds.toString(),
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
@ -291,7 +292,7 @@ fun SeekBar(
.padding(8.dp), .padding(8.dp),
) )
Text( Text(
text = "-" + ((player.duration - position) / 1000).seconds.toString(), text = "-$remaining",
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
modifier = modifier =

View file

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -56,9 +57,15 @@ import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.TimeFormatter
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import org.jellyfin.sdk.model.api.MediaSegmentDto import org.jellyfin.sdk.model.api.MediaSegmentDto
import org.jellyfin.sdk.model.api.TrickplayInfo import org.jellyfin.sdk.model.api.TrickplayInfo
import java.time.LocalTime
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
private val titleTextSize = 28.sp private val titleTextSize = 28.sp
private val subtitleTextSize = 18.sp private val subtitleTextSize = 18.sp
@ -74,6 +81,7 @@ fun PlaybackOverlay(
playerControls: Player, playerControls: Player,
controllerViewState: ControllerViewState, controllerViewState: ControllerViewState,
showPlay: Boolean, showPlay: Boolean,
showClock: Boolean,
previousEnabled: Boolean, previousEnabled: Boolean,
nextEnabled: Boolean, nextEnabled: Boolean,
seekEnabled: Boolean, seekEnabled: Boolean,
@ -144,6 +152,7 @@ fun PlaybackOverlay(
playerControls = playerControls, playerControls = playerControls,
controllerViewState = controllerViewState, controllerViewState = controllerViewState,
showPlay = showPlay, showPlay = showPlay,
showClock = showClock,
previousEnabled = previousEnabled, previousEnabled = previousEnabled,
nextEnabled = nextEnabled, nextEnabled = nextEnabled,
seekEnabled = seekEnabled, seekEnabled = seekEnabled,
@ -415,6 +424,7 @@ fun Controller(
subtitleStreams: List<SubtitleStream>, subtitleStreams: List<SubtitleStream>,
playerControls: Player, playerControls: Player,
controllerViewState: ControllerViewState, controllerViewState: ControllerViewState,
showClock: Boolean,
showPlay: Boolean, showPlay: Boolean,
previousEnabled: Boolean, previousEnabled: Boolean,
nextEnabled: Boolean, nextEnabled: Boolean,
@ -453,12 +463,40 @@ fun Controller(
fontSize = titleTextSize, fontSize = titleTextSize,
) )
} }
subtitle?.let { Row(
Text( horizontalArrangement = Arrangement.SpaceBetween,
text = it, modifier =
style = MaterialTheme.typography.titleMedium, Modifier
fontSize = subtitleTextSize, .fillMaxWidth()
) .align(Alignment.End),
) {
subtitle?.let {
Text(
text = it,
style = MaterialTheme.typography.titleMedium,
fontSize = subtitleTextSize,
)
}
if (showClock) {
var remaining by remember { mutableStateOf((playerControls.duration - playerControls.currentPosition).milliseconds) }
LaunchedEffect(playerControls) {
while (isActive) {
remaining =
(playerControls.duration - playerControls.currentPosition).milliseconds
delay(1.seconds)
}
}
val endTime = LocalTime.now().plusSeconds(remaining.inWholeSeconds)
val endTimeStr = TimeFormatter.format(endTime)
Text(
text = "Ends $endTimeStr",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelLarge,
modifier =
Modifier
.padding(end = 32.dp),
)
}
} }
} }
PlaybackControls( PlaybackControls(

View file

@ -387,6 +387,7 @@ fun PlaybackPage(
viewModel.playItemInPlaylist(it) viewModel.playItemInPlaylist(it)
}, },
currentSegment = currentSegment, currentSegment = currentSegment,
showClock = preferences.appPreferences.interfacePreferences.showClock,
) )
} }
} }

View file

@ -89,4 +89,11 @@ suspend fun upgradeApp(
} }
} }
} }
if (previous.isEqualOrBefore(Version.fromString("0.2.5-11-g0"))) {
appPreferences.updateData {
it.updateInterfacePreferences {
showClock = AppPreference.ShowClock.defaultValue
}
}
}
} }

View file

@ -18,8 +18,11 @@ import org.jellyfin.sdk.model.api.MediaStreamType
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
val TimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
/** /**
* Format a [LocalDateTime] as `Aug 24, 2000` * Format a [LocalDateTime] as `Aug 24, 2000`
*/ */

View file

@ -88,6 +88,7 @@ message InterfacePreferences {
map<string, int32> remembered_tabs = 3; map<string, int32> remembered_tabs = 3;
AppThemeColors app_theme_colors = 4; AppThemeColors app_theme_colors = 4;
bool nav_drawer_switch_on_focus = 5; bool nav_drawer_switch_on_focus = 5;
bool show_clock = 6;
} }
message AppPreferences { message AppPreferences {

View file

@ -138,6 +138,7 @@
<string name="watch_live">Watch live</string> <string name="watch_live">Watch live</string>
<string name="years_old">%1$d years old</string> <string name="years_old">%1$d years old</string>
<string name="play_with_transcoding">Play with transcoding</string> <string name="play_with_transcoding">Play with transcoding</string>
<string name="show_clock">Show Clock</string>
<string name="aired_episode_order">Aired Episode Order</string> <string name="aired_episode_order">Aired Episode Order</string>
<string name="create_playlist">Create new playlist</string> <string name="create_playlist">Create new playlist</string>
<string name="add_to_playlist">Add to playlist</string> <string name="add_to_playlist">Add to playlist</string>