mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Customize TV guide display (#443)
Adds a view options button for the TV guide to allow some basic customization: * Show details header * Adjust how channels are sorted * Toggle color coding for programs Related to #250 Closes #288 Fixes #439
This commit is contained in:
parent
0d8800863b
commit
6edcfb1067
10 changed files with 456 additions and 23 deletions
|
|
@ -784,6 +784,51 @@ sealed interface AppPreference<Pref, T> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val LiveTvShowHeader =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.show_details,
|
||||||
|
defaultValue = true,
|
||||||
|
getter = { it.interfacePreferences.liveTvPreferences.showHeader },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateLiveTvPreferences { showHeader = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.enabled,
|
||||||
|
summaryOff = R.string.disabled,
|
||||||
|
)
|
||||||
|
val LiveTvFavoriteChannelsBeginning =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.favorite_channels_at_beginning,
|
||||||
|
defaultValue = true,
|
||||||
|
getter = { it.interfacePreferences.liveTvPreferences.favoriteChannelsAtBeginning },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateLiveTvPreferences { favoriteChannelsAtBeginning = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.enabled,
|
||||||
|
summaryOff = R.string.disabled,
|
||||||
|
)
|
||||||
|
val LiveTvChannelSortByWatched =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.sort_channels_recently_watched,
|
||||||
|
defaultValue = false,
|
||||||
|
getter = { it.interfacePreferences.liveTvPreferences.sortByRecentlyWatched },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateLiveTvPreferences { sortByRecentlyWatched = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.enabled,
|
||||||
|
summaryOff = R.string.disabled,
|
||||||
|
)
|
||||||
|
val LiveTvColorCodePrograms =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.color_code_programs,
|
||||||
|
defaultValue = true,
|
||||||
|
getter = { it.interfacePreferences.liveTvPreferences.colorCodePrograms },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateLiveTvPreferences { colorCodePrograms = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.enabled,
|
||||||
|
summaryOff = R.string.disabled,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -948,6 +993,14 @@ val advancedPreferences =
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val liveTvPreferences =
|
||||||
|
listOf(
|
||||||
|
AppPreference.LiveTvShowHeader,
|
||||||
|
AppPreference.LiveTvFavoriteChannelsBeginning,
|
||||||
|
AppPreference.LiveTvChannelSortByWatched,
|
||||||
|
AppPreference.LiveTvColorCodePrograms,
|
||||||
|
)
|
||||||
|
|
||||||
data class AppSwitchPreference<Pref>(
|
data class AppSwitchPreference<Pref>(
|
||||||
@get:StringRes override val title: Int,
|
@get:StringRes override val title: Int,
|
||||||
override val defaultValue: Boolean,
|
override val defaultValue: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,19 @@ class AppPreferencesSerializer
|
||||||
.apply {
|
.apply {
|
||||||
resetSubtitles()
|
resetSubtitles()
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
liveTvPreferences =
|
||||||
|
LiveTvPreferences
|
||||||
|
.newBuilder()
|
||||||
|
.apply {
|
||||||
|
showHeader = AppPreference.LiveTvShowHeader.defaultValue
|
||||||
|
favoriteChannelsAtBeginning =
|
||||||
|
AppPreference.LiveTvFavoriteChannelsBeginning.defaultValue
|
||||||
|
sortByRecentlyWatched =
|
||||||
|
AppPreference.LiveTvChannelSortByWatched.defaultValue
|
||||||
|
colorCodePrograms =
|
||||||
|
AppPreference.LiveTvColorCodePrograms.defaultValue
|
||||||
|
}.build()
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
advancedPreferences =
|
advancedPreferences =
|
||||||
|
|
@ -155,6 +168,11 @@ inline fun AppPreferences.updateSubtitlePreferences(block: SubtitlePreferences.B
|
||||||
subtitlesPreferences = subtitlesPreferences.toBuilder().apply(block).build()
|
subtitlesPreferences = subtitlesPreferences.toBuilder().apply(block).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun AppPreferences.updateLiveTvPreferences(block: LiveTvPreferences.Builder.() -> Unit): AppPreferences =
|
||||||
|
updateInterfacePreferences {
|
||||||
|
liveTvPreferences = liveTvPreferences.toBuilder().apply(block).build()
|
||||||
|
}
|
||||||
|
|
||||||
inline fun AppPreferences.updateAdvancedPreferences(block: AdvancedPreferences.Builder.() -> Unit): AppPreferences =
|
inline fun AppPreferences.updateAdvancedPreferences(block: AdvancedPreferences.Builder.() -> Unit): AppPreferences =
|
||||||
update {
|
update {
|
||||||
advancedPreferences = advancedPreferences.toBuilder().apply(block).build()
|
advancedPreferences = advancedPreferences.toBuilder().apply(block).build()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.update
|
import com.github.damontecres.wholphin.preferences.update
|
||||||
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
|
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.updateLiveTvPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
||||||
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
|
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
|
||||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||||
|
|
@ -173,4 +174,17 @@ suspend fun upgradeApp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (previous.isEqualOrBefore(Version.fromString("0.3.5-56-g0"))) {
|
||||||
|
appPreferences.updateData {
|
||||||
|
it.updateLiveTvPreferences {
|
||||||
|
showHeader = AppPreference.LiveTvShowHeader.defaultValue
|
||||||
|
favoriteChannelsAtBeginning =
|
||||||
|
AppPreference.LiveTvFavoriteChannelsBeginning.defaultValue
|
||||||
|
sortByRecentlyWatched =
|
||||||
|
AppPreference.LiveTvChannelSortByWatched.defaultValue
|
||||||
|
colorCodePrograms =
|
||||||
|
AppPreference.LiveTvColorCodePrograms.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,17 @@ import coil3.compose.AsyncImage
|
||||||
fun Program(
|
fun Program(
|
||||||
program: TvProgram,
|
program: TvProgram,
|
||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
|
colorCode: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val background =
|
val background =
|
||||||
if (focused) {
|
if (focused) {
|
||||||
MaterialTheme.colorScheme.inverseSurface
|
MaterialTheme.colorScheme.inverseSurface
|
||||||
} else {
|
} else if (colorCode) {
|
||||||
program.category?.color
|
program.category?.color
|
||||||
?: MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
|
?: MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
|
||||||
}
|
}
|
||||||
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
|
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
|
||||||
Box(
|
Box(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -9,6 +10,7 @@ import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.WholphinApplication
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
|
|
@ -23,7 +25,13 @@ import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
@ -34,7 +42,9 @@ import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.GetProgramsDto
|
import org.jellyfin.sdk.model.api.GetProgramsDto
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
@ -45,11 +55,13 @@ import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
const val MAX_HOURS = 48L
|
const val MAX_HOURS = 48L
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LiveTvViewModel
|
class LiveTvViewModel
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -57,6 +69,7 @@ class LiveTvViewModel
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
|
val preferences: DataStore<AppPreferences>,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
@ -75,11 +88,25 @@ class LiveTvViewModel
|
||||||
|
|
||||||
private val range = 100
|
private val range = 100
|
||||||
|
|
||||||
fun init(firstLoad: Boolean) {
|
init {
|
||||||
guideStart = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
|
viewModelScope.launchIO {
|
||||||
if (!firstLoad) {
|
preferences.data
|
||||||
loading.value = LoadingState.Loading
|
.map {
|
||||||
|
it.interfacePreferences.liveTvPreferences.let {
|
||||||
|
Pair(it.sortByRecentlyWatched, it.favoriteChannelsAtBeginning)
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
.debounce { 500.milliseconds }
|
||||||
|
.collectLatest {
|
||||||
|
Timber.v("Init due to pref change")
|
||||||
|
loading.setValueOnMain(LoadingState.Pending)
|
||||||
|
init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
guideStart = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
|
||||||
viewModelScope.launch(
|
viewModelScope.launch(
|
||||||
Dispatchers.IO +
|
Dispatchers.IO +
|
||||||
LoadingExceptionHandler(
|
LoadingExceptionHandler(
|
||||||
|
|
@ -87,11 +114,26 @@ class LiveTvViewModel
|
||||||
"Could not fetch channels",
|
"Could not fetch channels",
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
val prefs =
|
||||||
|
(preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance())
|
||||||
|
.interfacePreferences.liveTvPreferences
|
||||||
val channelData by api.liveTvApi.getLiveTvChannels(
|
val channelData by api.liveTvApi.getLiveTvChannels(
|
||||||
GetLiveTvChannelsRequest(
|
GetLiveTvChannelsRequest(
|
||||||
startIndex = 0,
|
startIndex = 0,
|
||||||
userId = serverRepository.currentUser.value?.id,
|
userId = serverRepository.currentUser.value?.id,
|
||||||
enableFavoriteSorting = true,
|
enableFavoriteSorting = prefs.favoriteChannelsAtBeginning,
|
||||||
|
sortBy =
|
||||||
|
if (prefs.sortByRecentlyWatched) {
|
||||||
|
listOf(ItemSortBy.DATE_PLAYED)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
sortOrder =
|
||||||
|
if (prefs.sortByRecentlyWatched) {
|
||||||
|
SortOrder.DESCENDING
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
addCurrentProgram = false,
|
addCurrentProgram = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -148,6 +190,7 @@ class LiveTvViewModel
|
||||||
channelIds = channelsToFetch.map { it.id },
|
channelIds = channelsToFetch.map { it.id },
|
||||||
sortBy = listOf(ItemSortBy.START_DATE),
|
sortBy = listOf(ItemSortBy.START_DATE),
|
||||||
userId = serverRepository.currentUser.value?.id,
|
userId = serverRepository.currentUser.value?.id,
|
||||||
|
fields = listOf(ItemFields.OVERVIEW),
|
||||||
)
|
)
|
||||||
val fetchedPrograms =
|
val fetchedPrograms =
|
||||||
api.liveTvApi
|
api.liveTvApi
|
||||||
|
|
@ -173,6 +216,7 @@ class LiveTvViewModel
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val p =
|
val p =
|
||||||
TvProgram(
|
TvProgram(
|
||||||
id = dto.id,
|
id = dto.id,
|
||||||
|
|
@ -188,6 +232,8 @@ class LiveTvViewModel
|
||||||
duration = dto.runTimeTicks!!.ticks,
|
duration = dto.runTimeTicks!!.ticks,
|
||||||
name = dto.seriesName ?: dto.name,
|
name = dto.seriesName ?: dto.name,
|
||||||
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
||||||
|
overview = dto.overview,
|
||||||
|
officialRating = dto.officialRating,
|
||||||
seasonEpisode =
|
seasonEpisode =
|
||||||
if (dto.indexNumber != null && dto.parentIndexNumber != null) {
|
if (dto.indexNumber != null && dto.parentIndexNumber != null) {
|
||||||
SeasonEpisode(
|
SeasonEpisode(
|
||||||
|
|
@ -474,11 +520,13 @@ data class TvProgram(
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val subtitle: String?,
|
val subtitle: String?,
|
||||||
|
val overview: String? = null,
|
||||||
val seasonEpisode: SeasonEpisode?,
|
val seasonEpisode: SeasonEpisode?,
|
||||||
val isRecording: Boolean,
|
val isRecording: Boolean,
|
||||||
val isSeriesRecording: Boolean,
|
val isSeriesRecording: Boolean,
|
||||||
val isRepeat: Boolean,
|
val isRepeat: Boolean,
|
||||||
val category: ProgramCategory?,
|
val category: ProgramCategory?,
|
||||||
|
val officialRating: String? = null,
|
||||||
) {
|
) {
|
||||||
val isFake = category == ProgramCategory.FAKE
|
val isFake = category == ProgramCategory.FAKE
|
||||||
|
|
||||||
|
|
@ -500,6 +548,7 @@ data class TvProgram(
|
||||||
duration = ((endHours - startHours) * 60).toInt().minutes,
|
duration = ((endHours - startHours) * 60).toInt().minutes,
|
||||||
name = NO_DATA,
|
name = NO_DATA,
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
overview = null,
|
||||||
seasonEpisode = null,
|
seasonEpisode = null,
|
||||||
isRecording = false,
|
isRecording = false,
|
||||||
isSeriesRecording = false,
|
isSeriesRecording = false,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
|
import android.view.Gravity
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
|
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.AppPreference
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.liveTvPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.preferences.ComposablePreference
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveTvViewOptionsDialog(
|
||||||
|
preferences: AppPreferences,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onViewOptionsChange: (AppPreferences) -> Unit,
|
||||||
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
val columnState = rememberLazyListState()
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties =
|
||||||
|
DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
|
||||||
|
dialogWindowProvider?.window?.let { window ->
|
||||||
|
window.setGravity(Gravity.END)
|
||||||
|
window.setDimAmount(0f)
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
state = columnState,
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.width(256.dp)
|
||||||
|
.heightIn(max = 380.dp)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.view_options),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(liveTvPreferences) { pref ->
|
||||||
|
pref as AppPreference<AppPreferences, Any>
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val value = pref.getter.invoke(preferences)
|
||||||
|
ComposablePreference(
|
||||||
|
preference = pref,
|
||||||
|
value = value,
|
||||||
|
onNavigate = {},
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
onViewOptionsChange.invoke(pref.setter(preferences, newValue))
|
||||||
|
},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,13 @@ package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
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.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
|
@ -13,6 +16,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
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.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -24,6 +28,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
|
|
@ -39,10 +44,15 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.SurfaceDefaults
|
import androidx.tv.material3.SurfaceDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.LiveTvPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -66,16 +76,21 @@ fun TvGuideGrid(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: LiveTvViewModel = hiltViewModel(),
|
viewModel: LiveTvViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
var firstLoad by rememberSaveable { mutableStateOf(true) }
|
val scope = rememberCoroutineScope()
|
||||||
LaunchedEffect(Unit) {
|
// var firstLoad by rememberSaveable { mutableStateOf(true) }
|
||||||
viewModel.init(firstLoad)
|
// LaunchedEffect(Unit) {
|
||||||
firstLoad = false
|
// viewModel.init(firstLoad)
|
||||||
}
|
// firstLoad = false
|
||||||
|
// }
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
||||||
val channels by viewModel.channels.observeAsState(listOf())
|
val channels by viewModel.channels.observeAsState(listOf())
|
||||||
val programs by viewModel.programs.observeAsState(FetchedPrograms(0..0, listOf(), mapOf()))
|
val programs by viewModel.programs.observeAsState(FetchedPrograms(0..0, listOf(), mapOf()))
|
||||||
// val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
|
// val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
|
||||||
// val fetchedRange by viewModel.fetchedRange.observeAsState(0..0)
|
// val fetchedRange by viewModel.fetchedRange.observeAsState(0..0)
|
||||||
|
val preferences by viewModel.preferences.data
|
||||||
|
.collectAsState(AppPreferences.getDefaultInstance())
|
||||||
|
val tvPrefs = preferences.interfacePreferences.liveTvPreferences
|
||||||
|
var showViewOptions by remember { mutableStateOf(false) }
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> {
|
is LoadingState.Error -> {
|
||||||
ErrorMessage(state, modifier)
|
ErrorMessage(state, modifier)
|
||||||
|
|
@ -94,16 +109,49 @@ fun TvGuideGrid(
|
||||||
val loadingItem by viewModel.fetchingItem.observeAsState(LoadingState.Pending)
|
val loadingItem by viewModel.fetchingItem.observeAsState(LoadingState.Pending)
|
||||||
var showItemDialog by remember { mutableStateOf<Int?>(null) }
|
var showItemDialog by remember { mutableStateOf<Int?>(null) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val buttonFocusRequester = remember { FocusRequester() }
|
||||||
if (requestFocusAfterLoading) {
|
if (requestFocusAfterLoading) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.tryRequestFocus()
|
focusRequester.tryRequestFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(modifier = modifier) {
|
var focusedPosition by rememberPosition(0, 0)
|
||||||
|
val focusedProgram =
|
||||||
|
remember(focusedPosition) {
|
||||||
|
focusedPosition.let {
|
||||||
|
val channelId = channels.getOrNull(it.row)?.id
|
||||||
|
programs.programsByChannel[channelId]?.getOrNull(it.column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
if (channels.isEmpty()) {
|
if (channels.isEmpty()) {
|
||||||
ErrorMessage("Live TV is enabled, but no channels were found.", null)
|
ErrorMessage("Live TV is enabled, but no channels were found.", null)
|
||||||
} else {
|
} else {
|
||||||
|
AnimatedVisibility(tvPrefs.showHeader) {
|
||||||
|
TvGuideHeader(
|
||||||
|
program = focusedProgram,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
||||||
|
.fillMaxHeight(.30f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(focusedPosition.row < 1) {
|
||||||
|
ExpandableFaButton(
|
||||||
|
title = R.string.view_options,
|
||||||
|
iconStringRes = R.string.fa_sliders,
|
||||||
|
onClick = { showViewOptions = true },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = tvGuideDimensions.channelWidth)
|
||||||
|
.focusRequester(buttonFocusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
TvGuideGridContent(
|
TvGuideGridContent(
|
||||||
|
preferences = tvPrefs,
|
||||||
loading = state is LoadingState.Loading,
|
loading = state is LoadingState.Loading,
|
||||||
channels = channels,
|
channels = channels,
|
||||||
programs = programs,
|
programs = programs,
|
||||||
|
|
@ -118,6 +166,7 @@ fun TvGuideGrid(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onFocus = {
|
onFocus = {
|
||||||
|
focusedPosition = it
|
||||||
onRowPosition.invoke(it.row)
|
onRowPosition.invoke(it.row)
|
||||||
viewModel.onFocusChannel(it)
|
viewModel.onFocusChannel(it)
|
||||||
},
|
},
|
||||||
|
|
@ -144,6 +193,7 @@ fun TvGuideGrid(
|
||||||
showItemDialog = index
|
showItemDialog = index
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
buttonFocusRequester = buttonFocusRequester,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -192,10 +242,33 @@ fun TvGuideGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showViewOptions) {
|
||||||
|
LiveTvViewOptionsDialog(
|
||||||
|
preferences = preferences,
|
||||||
|
onDismissRequest = { showViewOptions = false },
|
||||||
|
onViewOptionsChange = { newPrefs ->
|
||||||
|
scope.launchIO {
|
||||||
|
viewModel.preferences.updateData {
|
||||||
|
newPrefs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val tvGuideDimensions =
|
||||||
|
ProgramGuideDimensions(
|
||||||
|
timelineHourWidth = 240.dp,
|
||||||
|
timelineHeight = 32.dp,
|
||||||
|
channelWidth = 120.dp,
|
||||||
|
channelHeight = 64.dp,
|
||||||
|
currentTimeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvGuideGridContent(
|
fun TvGuideGridContent(
|
||||||
|
preferences: LiveTvPreferences,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
channels: List<TvChannel>,
|
channels: List<TvChannel>,
|
||||||
programs: FetchedPrograms,
|
programs: FetchedPrograms,
|
||||||
|
|
@ -204,6 +277,7 @@ fun TvGuideGridContent(
|
||||||
onClickChannel: (Int, TvChannel) -> Unit,
|
onClickChannel: (Int, TvChannel) -> Unit,
|
||||||
onClickProgram: (Int, TvProgram) -> Unit,
|
onClickProgram: (Int, TvProgram) -> Unit,
|
||||||
onFocus: (RowColumn) -> Unit,
|
onFocus: (RowColumn) -> Unit,
|
||||||
|
buttonFocusRequester: FocusRequester,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
@ -229,25 +303,19 @@ fun TvGuideGridContent(
|
||||||
state.animateToProgram(focusedProgramIndex, Alignment.Center)
|
state.animateToProgram(focusedProgramIndex, Alignment.Center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val dimensions =
|
|
||||||
ProgramGuideDimensions(
|
|
||||||
timelineHourWidth = 240.dp,
|
|
||||||
timelineHeight = 32.dp,
|
|
||||||
channelWidth = 120.dp,
|
|
||||||
channelHeight = 64.dp,
|
|
||||||
currentTimeWidth = 2.dp,
|
|
||||||
)
|
|
||||||
var gridHasFocus by rememberSaveable { mutableStateOf(false) }
|
var gridHasFocus by rememberSaveable { mutableStateOf(false) }
|
||||||
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
|
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
ProgramGuide(
|
ProgramGuide(
|
||||||
state = state,
|
state = state,
|
||||||
dimensions = dimensions,
|
dimensions = tvGuideDimensions,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
gridHasFocus = it.hasFocus
|
gridHasFocus = it.hasFocus
|
||||||
|
}.focusProperties {
|
||||||
|
up = buttonFocusRequester
|
||||||
}.focusable()
|
}.focusable()
|
||||||
.onPreviewKeyEvent {
|
.onPreviewKeyEvent {
|
||||||
if (it.type == KeyEventType.KeyUp) {
|
if (it.type == KeyEventType.KeyUp) {
|
||||||
|
|
@ -291,7 +359,7 @@ fun TvGuideGridContent(
|
||||||
|
|
||||||
Key.DirectionUp -> {
|
Key.DirectionUp -> {
|
||||||
if (item.row <= 0) {
|
if (item.row <= 0) {
|
||||||
focusManager.moveFocus(FocusDirection.Up)
|
// focusManager.moveFocus(FocusDirection.Up)
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
val newChannelIndex = item.row - 1
|
val newChannelIndex = item.row - 1
|
||||||
|
|
@ -516,7 +584,7 @@ fun TvGuideGridContent(
|
||||||
val program = programs.programs[programIndex]
|
val program = programs.programs[programIndex]
|
||||||
val focused =
|
val focused =
|
||||||
gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex
|
gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex
|
||||||
Program(program, focused, Modifier)
|
Program(program, focused, preferences.colorCodePrograms, Modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
channels(
|
channels(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
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.ui.components.DotSeparatedRow
|
||||||
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
|
import com.github.damontecres.wholphin.ui.components.StreamLabel
|
||||||
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import kotlin.time.toKotlinDuration
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvGuideHeader(
|
||||||
|
program: TvProgram?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
val details =
|
||||||
|
remember(program) {
|
||||||
|
buildList {
|
||||||
|
program?.let {
|
||||||
|
val differentDay = it.start.toLocalDate() != now.toLocalDate()
|
||||||
|
val time =
|
||||||
|
DateUtils.formatDateRange(
|
||||||
|
context,
|
||||||
|
it.start
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.epochSecond * 1000,
|
||||||
|
it.end
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.epochSecond * 1000,
|
||||||
|
DateUtils.FORMAT_SHOW_TIME or if (differentDay) DateUtils.FORMAT_SHOW_WEEKDAY else 0,
|
||||||
|
)
|
||||||
|
add(time)
|
||||||
|
}
|
||||||
|
if (program?.isFake == false) {
|
||||||
|
program
|
||||||
|
.duration
|
||||||
|
.roundMinutes
|
||||||
|
.toString()
|
||||||
|
.let(::add)
|
||||||
|
if (now.isAfter(program.start) && now.isBefore(program.end)) {
|
||||||
|
java.time.Duration
|
||||||
|
.between(now, program.end)
|
||||||
|
.toKotlinDuration()
|
||||||
|
.roundMinutes
|
||||||
|
.let { add("$it left") }
|
||||||
|
}
|
||||||
|
program.seasonEpisode?.let { "S${it.season} E${it.episode}" }?.let(::add)
|
||||||
|
program.officialRating?.let(::add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = program?.name ?: program?.id.toString(),
|
||||||
|
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(.75f),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(.6f),
|
||||||
|
) {
|
||||||
|
program?.subtitle?.let {
|
||||||
|
Text(
|
||||||
|
text = program.subtitle,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
DotSeparatedRow(
|
||||||
|
texts = details,
|
||||||
|
communityRating = null,
|
||||||
|
criticRating = null,
|
||||||
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
if (program?.isRepeat == true) {
|
||||||
|
StreamLabel(stringResource(R.string.live_tv_repeat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OverviewText(
|
||||||
|
overview = program?.overview ?: "",
|
||||||
|
maxLines = 3,
|
||||||
|
onClick = {},
|
||||||
|
enabled = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -125,6 +125,13 @@ message SubtitlePreferences{
|
||||||
int32 edge_thickness = 12;
|
int32 edge_thickness = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LiveTvPreferences {
|
||||||
|
bool show_header = 1;
|
||||||
|
bool favorite_channels_at_beginning = 2;
|
||||||
|
bool sort_by_recently_watched = 3;
|
||||||
|
bool color_code_programs = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message InterfacePreferences {
|
message InterfacePreferences {
|
||||||
ThemeSongVolume play_theme_songs = 1;
|
ThemeSongVolume play_theme_songs = 1;
|
||||||
bool remember_selected_tab = 2;
|
bool remember_selected_tab = 2;
|
||||||
|
|
@ -133,6 +140,7 @@ message InterfacePreferences {
|
||||||
bool nav_drawer_switch_on_focus = 5;
|
bool nav_drawer_switch_on_focus = 5;
|
||||||
bool show_clock = 6;
|
bool show_clock = 6;
|
||||||
SubtitlePreferences subtitles_preferences = 7;
|
SubtitlePreferences subtitles_preferences = 7;
|
||||||
|
LiveTvPreferences live_tv_preferences = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdvancedPreferences {
|
message AdvancedPreferences {
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,10 @@
|
||||||
<string name="bit_unit">bit</string>
|
<string name="bit_unit">bit</string>
|
||||||
<string name="sample_rate_unit">Hz</string>
|
<string name="sample_rate_unit">Hz</string>
|
||||||
<string name="show_titles">Show titles</string>
|
<string name="show_titles">Show titles</string>
|
||||||
|
<string name="live_tv_repeat">Repeat</string>
|
||||||
|
<string name="favorite_channels_at_beginning">Show favorite channels first</string>
|
||||||
|
<string name="sort_channels_recently_watched">Sort channels by recently watched</string>
|
||||||
|
<string name="color_code_programs">Color-code programs</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue