Merge Wholphin v1.1.0 while preserving Wolfskin features #27
7 changed files with 106 additions and 16 deletions
|
|
@ -9,6 +9,7 @@ import com.github.damontecres.wholphin.BuildConfig
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.WholphinApplication
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||||
|
import com.github.damontecres.wholphin.ui.formatMaxBitrate
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.ConditionalPreferences
|
import com.github.damontecres.wholphin.ui.preferences.ConditionalPreferences
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
|
||||||
|
|
@ -345,7 +346,7 @@ sealed interface AppPreference<Pref, T> {
|
||||||
|
|
||||||
const val MEGA_BIT = 1024 * 1024L
|
const val MEGA_BIT = 1024 * 1024L
|
||||||
const val DEFAULT_BITRATE = 100 * MEGA_BIT
|
const val DEFAULT_BITRATE = 100 * MEGA_BIT
|
||||||
private val bitrateValues =
|
val bitrateValues =
|
||||||
listOf(
|
listOf(
|
||||||
500 * 1024L,
|
500 * 1024L,
|
||||||
750 * 1024L,
|
750 * 1024L,
|
||||||
|
|
@ -378,11 +379,7 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summarizer = { value ->
|
summarizer = { value ->
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
val v = bitrateValues.getOrNull(value.toInt()) ?: DEFAULT_BITRATE
|
val v = bitrateValues.getOrNull(value.toInt()) ?: DEFAULT_BITRATE
|
||||||
if (v < MEGA_BIT) {
|
formatMaxBitrate(v)
|
||||||
"${v / 1024} kbps"
|
|
||||||
} else {
|
|
||||||
"${v / MEGA_BIT} Mbps"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.WholphinApplication
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreference.Companion.MEGA_BIT
|
||||||
import com.github.damontecres.wholphin.ui.setup.ServerVersionSupported
|
import com.github.damontecres.wholphin.ui.setup.ServerVersionSupported
|
||||||
import org.jellyfin.sdk.model.ServerVersion
|
import org.jellyfin.sdk.model.ServerVersion
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
|
@ -208,6 +209,13 @@ fun formatBytes(
|
||||||
|
|
||||||
fun formatBitrate(bitrate: Int) = formatBytes(bitrate.toLong(), byteRateSuffixes, 1000)
|
fun formatBitrate(bitrate: Int) = formatBytes(bitrate.toLong(), byteRateSuffixes, 1000)
|
||||||
|
|
||||||
|
fun formatMaxBitrate(bitrate: Long) =
|
||||||
|
if (bitrate < MEGA_BIT) {
|
||||||
|
"${bitrate / 1024} kbps"
|
||||||
|
} else {
|
||||||
|
"${bitrate / MEGA_BIT} Mbps"
|
||||||
|
}
|
||||||
|
|
||||||
@get:StringRes
|
@get:StringRes
|
||||||
val MediaSegmentType.stringRes: Int
|
val MediaSegmentType.stringRes: Int
|
||||||
get() =
|
get() =
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,10 @@ import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||||
|
import com.github.damontecres.wholphin.ui.formatMaxBitrate
|
||||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialog
|
import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialog
|
||||||
import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialogItem
|
import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialogItem
|
||||||
|
|
@ -60,6 +62,7 @@ enum class PlaybackDialogType {
|
||||||
PLAYBACK_SPEED,
|
PLAYBACK_SPEED,
|
||||||
VIDEO_SCALE,
|
VIDEO_SCALE,
|
||||||
SUBTITLE_DELAY,
|
SUBTITLE_DELAY,
|
||||||
|
BITRATE,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PlaybackSettings(
|
data class PlaybackSettings(
|
||||||
|
|
@ -73,6 +76,7 @@ data class PlaybackSettings(
|
||||||
val subtitleDelay: Duration,
|
val subtitleDelay: Duration,
|
||||||
val hasSubtitleDownloadPermission: Boolean,
|
val hasSubtitleDownloadPermission: Boolean,
|
||||||
val playbackSpeedEnabled: Boolean,
|
val playbackSpeedEnabled: Boolean,
|
||||||
|
val maxBitrate: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -162,6 +166,13 @@ fun PlaybackDialog(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
add(
|
||||||
|
BottomDialogItem(
|
||||||
|
data = PlaybackDialogType.BITRATE,
|
||||||
|
headline = stringResource(R.string.max_bitrate),
|
||||||
|
supporting = remember(settings.maxBitrate) { formatMaxBitrate(settings.maxBitrate) },
|
||||||
|
),
|
||||||
|
)
|
||||||
add(
|
add(
|
||||||
BottomDialogItem(
|
BottomDialogItem(
|
||||||
data = PlaybackDialogType.DEBUG,
|
data = PlaybackDialogType.DEBUG,
|
||||||
|
|
@ -271,6 +282,29 @@ fun PlaybackDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlaybackDialogType.BITRATE -> {
|
||||||
|
val choices =
|
||||||
|
AppPreference.bitrateValues.map { bitrate ->
|
||||||
|
BottomDialogItem(
|
||||||
|
data = bitrate,
|
||||||
|
headline = formatMaxBitrate(bitrate),
|
||||||
|
supporting = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BottomDialog(
|
||||||
|
choices = choices,
|
||||||
|
currentChoice = choices.firstOrNull { it.data == settings.maxBitrate },
|
||||||
|
onDismissRequest = {
|
||||||
|
onControllerInteraction.invoke()
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
},
|
||||||
|
onSelectChoice = { _, choice ->
|
||||||
|
onPlaybackActionClick.invoke(PlaybackAction.ChangeBitrate(choice.data))
|
||||||
|
},
|
||||||
|
gravity = leftGravity,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,10 @@ fun PlaybackPageContent(
|
||||||
player.seekToPrevious()
|
player.seekToPrevious()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is PlaybackAction.ChangeBitrate -> {
|
||||||
|
viewModel.changeMaxBitrate(it.maxBitrate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -752,6 +756,7 @@ fun PlaybackPageContent(
|
||||||
// TODO Passing through audio prevents changing playback speed
|
// TODO Passing through audio prevents changing playback speed
|
||||||
// See https://github.com/damontecres/Wholphin/issues/164
|
// See https://github.com/damontecres/Wholphin/issues/164
|
||||||
playbackSpeedEnabled = playerBackend == PlayerBackend.MPV || state.currentPlayback?.audioDecoder != null,
|
playbackSpeedEnabled = playerBackend == PlayerBackend.MPV || state.currentPlayback?.audioDecoder != null,
|
||||||
|
maxBitrate = state.maxBitrate,
|
||||||
),
|
),
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
playbackDialog =
|
playbackDialog =
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ data class PlaybackState(
|
||||||
val nextUp: BaseItem? = null,
|
val nextUp: BaseItem? = null,
|
||||||
val playlistIndex: Int = 0,
|
val playlistIndex: Int = 0,
|
||||||
val playlist: Playlist = Playlist(emptyList()),
|
val playlist: Playlist = Playlist(emptyList()),
|
||||||
|
val maxBitrate: Long = -1L,
|
||||||
) {
|
) {
|
||||||
val hasNext: Boolean get() = (playlistIndex + 1) < playlist.items.size
|
val hasNext: Boolean get() = (playlistIndex + 1) < playlist.items.size
|
||||||
val hasPrevious: Boolean get() = playlistIndex > 0
|
val hasPrevious: Boolean get() = playlistIndex > 0
|
||||||
|
|
|
||||||
|
|
@ -292,8 +292,16 @@ class PlaybackViewModel
|
||||||
*/
|
*/
|
||||||
private suspend fun init() {
|
private suspend fun init() {
|
||||||
musicService.stop()
|
musicService.stop()
|
||||||
_state.update { it.copy(nextUp = null) }
|
|
||||||
this.preferences = userPreferencesService.getCurrent()
|
this.preferences = userPreferencesService.getCurrent()
|
||||||
|
val maxBitrate =
|
||||||
|
preferences.appPreferences.playbackPreferences.maxBitrate
|
||||||
|
.takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
nextUp = null,
|
||||||
|
maxBitrate = maxBitrate,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (preferences.appPreferences.playbackPreferences.refreshRateSwitching) {
|
if (preferences.appPreferences.playbackPreferences.refreshRateSwitching) {
|
||||||
addCloseable { refreshRateService.resetRefreshRate() }
|
addCloseable { refreshRateService.resetRefreshRate() }
|
||||||
}
|
}
|
||||||
|
|
@ -628,6 +636,7 @@ class PlaybackViewModel
|
||||||
positionMs: Long = 0,
|
positionMs: Long = 0,
|
||||||
enableDirectPlay: Boolean = !this.forceTranscoding,
|
enableDirectPlay: Boolean = !this.forceTranscoding,
|
||||||
enableDirectStream: Boolean = !this.forceTranscoding,
|
enableDirectStream: Boolean = !this.forceTranscoding,
|
||||||
|
maxBitrate: Long = this.state.value.maxBitrate,
|
||||||
): Unit =
|
): Unit =
|
||||||
withContext(WholphinDispatchers.IO) {
|
withContext(WholphinDispatchers.IO) {
|
||||||
val itemId = item.id
|
val itemId = item.id
|
||||||
|
|
@ -638,7 +647,8 @@ class PlaybackViewModel
|
||||||
state.value.currentPlayback?.let { currentPlayback ->
|
state.value.currentPlayback?.let { currentPlayback ->
|
||||||
if (currentPlayback.item.id == item.id &&
|
if (currentPlayback.item.id == item.id &&
|
||||||
currentPlayback.playMethod == PlayMethod.DIRECT_PLAY &&
|
currentPlayback.playMethod == PlayMethod.DIRECT_PLAY &&
|
||||||
enableDirectPlay
|
enableDirectPlay &&
|
||||||
|
state.value.maxBitrate == maxBitrate
|
||||||
) {
|
) {
|
||||||
val wasSuccessful =
|
val wasSuccessful =
|
||||||
changeStreamsDirectPlay(
|
changeStreamsDirectPlay(
|
||||||
|
|
@ -651,18 +661,17 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.i(
|
Timber.i(
|
||||||
"changeStreams (%s): audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s",
|
"changeStreams (%s): audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s, maxBitrate=%s",
|
||||||
itemId,
|
itemId,
|
||||||
audioIndex,
|
audioIndex,
|
||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
enableDirectPlay,
|
enableDirectPlay,
|
||||||
enableDirectStream,
|
enableDirectStream,
|
||||||
positionMs,
|
positionMs,
|
||||||
|
maxBitrate,
|
||||||
)
|
)
|
||||||
|
_state.update { it.copy(maxBitrate = maxBitrate) }
|
||||||
|
|
||||||
val maxBitrate =
|
|
||||||
preferences.appPreferences.playbackPreferences.maxBitrate
|
|
||||||
.takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE
|
|
||||||
val response by
|
val response by
|
||||||
api.mediaInfoApi
|
api.mediaInfoApi
|
||||||
.getPostedPlaybackInfo(
|
.getPostedPlaybackInfo(
|
||||||
|
|
@ -1021,6 +1030,26 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeMaxBitrate(newMaxBitrate: Long) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val currentPlayback = state.value.currentPlayback
|
||||||
|
if (currentPlayback != null) {
|
||||||
|
Timber.d("Changing bitrate to %s", newMaxBitrate)
|
||||||
|
changeStreams(
|
||||||
|
item = currentItem.item,
|
||||||
|
sourceId = currentPlayback.mediaSourceInfo.id,
|
||||||
|
audioIndex = currentPlayback.audioIndex,
|
||||||
|
subtitleIndex = currentPlayback.subtitleIndex,
|
||||||
|
positionMs = onMain { player.currentPosition },
|
||||||
|
enableDirectPlay = true,
|
||||||
|
maxBitrate = newMaxBitrate,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Timber.w("Trying to change bitrate, but currentPlayback is null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal suspend fun saveTrackSelection(
|
internal suspend fun saveTrackSelection(
|
||||||
trackIndex: Int,
|
trackIndex: Int,
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
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.relocation.BringIntoViewRequester
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|
@ -122,6 +123,10 @@ sealed interface PlaybackAction {
|
||||||
data object Previous : PlaybackAction
|
data object Previous : PlaybackAction
|
||||||
|
|
||||||
data object Next : PlaybackAction
|
data object Next : PlaybackAction
|
||||||
|
|
||||||
|
data class ChangeBitrate(
|
||||||
|
val maxBitrate: Long,
|
||||||
|
) : PlaybackAction
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -561,12 +566,19 @@ fun <T> BottomDialog(
|
||||||
gravity: Int,
|
gravity: Int,
|
||||||
currentChoice: BottomDialogItem<T>? = null,
|
currentChoice: BottomDialogItem<T>? = null,
|
||||||
) {
|
) {
|
||||||
|
val initialIndex =
|
||||||
|
remember(choices, currentChoice) { choices.indexOfFirstOrNull { it == currentChoice } } ?: 0
|
||||||
|
val listState =
|
||||||
|
rememberLazyListState(
|
||||||
|
initialFirstVisibleItemIndex = initialIndex,
|
||||||
|
)
|
||||||
val focusRequesters = remember(choices.size) { List(choices.size) { FocusRequester() } }
|
val focusRequesters = remember(choices.size) { List(choices.size) { FocusRequester() } }
|
||||||
|
val bringIntoViewRequesters =
|
||||||
|
remember(choices.size) { List(choices.size) { BringIntoViewRequester() } }
|
||||||
if (currentChoice != null) {
|
if (currentChoice != null) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
choices.indexOfFirstOrNull { it == currentChoice }?.let {
|
bringIntoViewRequesters.getOrNull(initialIndex)?.bringIntoView()
|
||||||
focusRequesters.getOrNull(it)?.tryRequestFocus()
|
focusRequesters.getOrNull(initialIndex)?.tryRequestFocus()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO enforcing a width ends up ignore the gravity
|
// TODO enforcing a width ends up ignore the gravity
|
||||||
|
|
@ -591,6 +603,7 @@ fun <T> BottomDialog(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -624,7 +637,10 @@ fun <T> BottomDialog(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
modifier = Modifier.focusRequester(focusRequesters[index]),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.bringIntoViewRequester(bringIntoViewRequesters[index])
|
||||||
|
.focusRequester(focusRequesters[index]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue