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.WholphinApplication
|
||||
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.preferences.ConditionalPreferences
|
||||
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 DEFAULT_BITRATE = 100 * MEGA_BIT
|
||||
private val bitrateValues =
|
||||
val bitrateValues =
|
||||
listOf(
|
||||
500 * 1024L,
|
||||
750 * 1024L,
|
||||
|
|
@ -378,11 +379,7 @@ sealed interface AppPreference<Pref, T> {
|
|||
summarizer = { value ->
|
||||
if (value != null) {
|
||||
val v = bitrateValues.getOrNull(value.toInt()) ?: DEFAULT_BITRATE
|
||||
if (v < MEGA_BIT) {
|
||||
"${v / 1024} kbps"
|
||||
} else {
|
||||
"${v / MEGA_BIT} Mbps"
|
||||
}
|
||||
formatMaxBitrate(v)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.text.AnnotatedString
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import com.github.damontecres.wholphin.R
|
||||
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 org.jellyfin.sdk.model.ServerVersion
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
|
@ -208,6 +209,13 @@ fun formatBytes(
|
|||
|
||||
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
|
||||
val MediaSegmentType.stringRes: Int
|
||||
get() =
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ import androidx.tv.material3.Text
|
|||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
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.components.SelectedLeadingContent
|
||||
import com.github.damontecres.wholphin.ui.formatMaxBitrate
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialog
|
||||
import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialogItem
|
||||
|
|
@ -60,6 +62,7 @@ enum class PlaybackDialogType {
|
|||
PLAYBACK_SPEED,
|
||||
VIDEO_SCALE,
|
||||
SUBTITLE_DELAY,
|
||||
BITRATE,
|
||||
}
|
||||
|
||||
data class PlaybackSettings(
|
||||
|
|
@ -73,6 +76,7 @@ data class PlaybackSettings(
|
|||
val subtitleDelay: Duration,
|
||||
val hasSubtitleDownloadPermission: 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(
|
||||
BottomDialogItem(
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
is PlaybackAction.ChangeBitrate -> {
|
||||
viewModel.changeMaxBitrate(it.maxBitrate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -752,6 +756,7 @@ fun PlaybackPageContent(
|
|||
// TODO Passing through audio prevents changing playback speed
|
||||
// See https://github.com/damontecres/Wholphin/issues/164
|
||||
playbackSpeedEnabled = playerBackend == PlayerBackend.MPV || state.currentPlayback?.audioDecoder != null,
|
||||
maxBitrate = state.maxBitrate,
|
||||
),
|
||||
onDismissRequest = {
|
||||
playbackDialog =
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ data class PlaybackState(
|
|||
val nextUp: BaseItem? = null,
|
||||
val playlistIndex: Int = 0,
|
||||
val playlist: Playlist = Playlist(emptyList()),
|
||||
val maxBitrate: Long = -1L,
|
||||
) {
|
||||
val hasNext: Boolean get() = (playlistIndex + 1) < playlist.items.size
|
||||
val hasPrevious: Boolean get() = playlistIndex > 0
|
||||
|
|
|
|||
|
|
@ -292,8 +292,16 @@ class PlaybackViewModel
|
|||
*/
|
||||
private suspend fun init() {
|
||||
musicService.stop()
|
||||
_state.update { it.copy(nextUp = null) }
|
||||
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) {
|
||||
addCloseable { refreshRateService.resetRefreshRate() }
|
||||
}
|
||||
|
|
@ -628,6 +636,7 @@ class PlaybackViewModel
|
|||
positionMs: Long = 0,
|
||||
enableDirectPlay: Boolean = !this.forceTranscoding,
|
||||
enableDirectStream: Boolean = !this.forceTranscoding,
|
||||
maxBitrate: Long = this.state.value.maxBitrate,
|
||||
): Unit =
|
||||
withContext(WholphinDispatchers.IO) {
|
||||
val itemId = item.id
|
||||
|
|
@ -638,7 +647,8 @@ class PlaybackViewModel
|
|||
state.value.currentPlayback?.let { currentPlayback ->
|
||||
if (currentPlayback.item.id == item.id &&
|
||||
currentPlayback.playMethod == PlayMethod.DIRECT_PLAY &&
|
||||
enableDirectPlay
|
||||
enableDirectPlay &&
|
||||
state.value.maxBitrate == maxBitrate
|
||||
) {
|
||||
val wasSuccessful =
|
||||
changeStreamsDirectPlay(
|
||||
|
|
@ -651,18 +661,17 @@ class PlaybackViewModel
|
|||
}
|
||||
|
||||
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,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
enableDirectPlay,
|
||||
enableDirectStream,
|
||||
positionMs,
|
||||
maxBitrate,
|
||||
)
|
||||
_state.update { it.copy(maxBitrate = maxBitrate) }
|
||||
|
||||
val maxBitrate =
|
||||
preferences.appPreferences.playbackPreferences.maxBitrate
|
||||
.takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE
|
||||
val response by
|
||||
api.mediaInfoApi
|
||||
.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(
|
||||
trackIndex: Int,
|
||||
type: MediaStreamType,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.wrapContentSize
|
|||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.shape.RoundedCornerShape
|
||||
|
|
@ -122,6 +123,10 @@ sealed interface PlaybackAction {
|
|||
data object Previous : PlaybackAction
|
||||
|
||||
data object Next : PlaybackAction
|
||||
|
||||
data class ChangeBitrate(
|
||||
val maxBitrate: Long,
|
||||
) : PlaybackAction
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -561,12 +566,19 @@ fun <T> BottomDialog(
|
|||
gravity: Int,
|
||||
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 bringIntoViewRequesters =
|
||||
remember(choices.size) { List(choices.size) { BringIntoViewRequester() } }
|
||||
if (currentChoice != null) {
|
||||
LaunchedEffect(Unit) {
|
||||
choices.indexOfFirstOrNull { it == currentChoice }?.let {
|
||||
focusRequesters.getOrNull(it)?.tryRequestFocus()
|
||||
}
|
||||
bringIntoViewRequesters.getOrNull(initialIndex)?.bringIntoView()
|
||||
focusRequesters.getOrNull(initialIndex)?.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
// TODO enforcing a width ends up ignore the gravity
|
||||
|
|
@ -591,6 +603,7 @@ fun <T> BottomDialog(
|
|||
),
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -624,7 +637,10 @@ fun <T> BottomDialog(
|
|||
}
|
||||
},
|
||||
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