Several UI/UX minor changes (#1623)

## Description
Gathering some smaller UI/UX changes:
- Default to the user's preferred subtitle language when
searching/downloading subtitles and if unset, fall back to the UI's
language
- Only show >0s runtime on quick details & don't show negative ends at
- On the OS channel row for recently added, only include movies &
episodes
- Only show movies & episodes to "Recently Added" Android OS channel
- Fix context menu for adding songs & albums to the music queue

### Related issues
Related to #1339 & #1484
Fixes #1617
Fixes
https://github.com/damontecres/Wholphin/issues/1329#issuecomment-4440684328

### Testing
Just on the emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-06-26 12:43:56 -04:00 • committed by GitHub
parent e5887dfa15
commit f5ce9745ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 41 additions and 5 deletions

View file

@ -160,6 +160,7 @@ data class BaseItem(
data.runTimeTicks data.runTimeTicks
?.ticks ?.ticks
?.roundMinutes ?.roundMinutes
?.takeIf { it > Duration.ZERO }
?.let { add(it.toString()) } ?.let { add(it.toString()) }
data.timeRemaining data.timeRemaining
?.roundMinutes ?.roundMinutes

View file

@ -228,6 +228,8 @@ class MusicService
start() start()
} }
} }
} else {
Timber.w("Tried to add non-audio type %s: %s", item.type, item.id)
} }
} }

View file

@ -264,6 +264,7 @@ class TvProviderWorker
.getLatestMedia( .getLatestMedia(
GetLatestMediaRequest( GetLatestMediaRequest(
fields = SlimItemFields, fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
imageTypeLimit = 1, imageTypeLimit = 1,
parentId = null, parentId = null,
groupItems = true, groupItems = true,

View file

@ -244,7 +244,7 @@ val BaseItemDto.timeRemaining: Duration?
get() = get() =
userData?.playbackPositionTicks?.let { userData?.playbackPositionTicks?.let {
if (it > 0) { if (it > 0) {
runTimeTicks?.minus(it)?.ticks runTimeTicks?.minus(it)?.ticks?.takeIf { it > Duration.ZERO }
} else { } else {
null null
} }

View file

@ -339,7 +339,8 @@ private fun buildContextMenuItems(
onClickPlayWith: () -> Unit, onClickPlayWith: () -> Unit,
): List<DialogItem> = ): List<DialogItem> =
buildList { buildList {
if (showGoTo) { // Songs should not show Go to
if (showGoTo && item.type != BaseItemKind.AUDIO) {
add( add(
DialogItem( DialogItem(
resources.getString(R.string.go_to), resources.getString(R.string.go_to),
@ -466,7 +467,7 @@ private fun buildContextMenuItems(
} }
} }
} }
if (item.type == BaseItemKind.MUSIC_ALBUM) { if (item.type == BaseItemKind.MUSIC_ALBUM || item.type == BaseItemKind.AUDIO) {
add( add(
DialogItem( DialogItem(
resources.getString(R.string.add_to_queue), resources.getString(R.string.add_to_queue),

View file

@ -91,6 +91,7 @@ class ContextMenuUtils(
onClearChosenStreams = { onClearChosenStreams = {
// Not supported on this page // Not supported on this page
}, },
onClickAddToQueue = playlistViewModel::addToQueue,
) )
} }
val contextMenu = val contextMenu =

View file

@ -5,14 +5,19 @@ import android.widget.Toast
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.services.MusicService
import com.github.damontecres.wholphin.services.PlaylistCreator import com.github.damontecres.wholphin.services.PlaylistCreator
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.music.addToQueue
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
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.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
@ -27,7 +32,9 @@ class AddPlaylistViewModel
@Inject @Inject
constructor( constructor(
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
private val api: ApiClient,
private val playlistCreator: PlaylistCreator, private val playlistCreator: PlaylistCreator,
private val musicService: MusicService,
) : ViewModel() { ) : ViewModel() {
val playlistState = MutableStateFlow<PlaylistLoadingState>(PlaylistLoadingState.Pending) val playlistState = MutableStateFlow<PlaylistLoadingState>(PlaylistLoadingState.Pending)
@ -72,4 +79,10 @@ class AddPlaylistViewModel
} }
} }
} }
fun addToQueue(item: BaseItem) {
viewModelScope.launchDefault {
addToQueue(api, musicService, item, -1)
}
}
} }

View file

@ -147,3 +147,11 @@ fun ViewModel.addToQueue(
} }
} }
} }
suspend fun addToQueue(
api: ApiClient,
musicService: MusicService,
item: BaseItem,
index: Int,
) {
}

View file

@ -139,7 +139,7 @@ class PlaybackViewModel
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
private val playlistCreator: PlaylistCreator, private val playlistCreator: PlaylistCreator,
private val itemPlaybackDao: ItemPlaybackDao, private val itemPlaybackDao: ItemPlaybackDao,
private val serverRepository: ServerRepository, internal val serverRepository: ServerRepository,
private val itemPlaybackRepository: ItemPlaybackRepository, private val itemPlaybackRepository: ItemPlaybackRepository,
private val playerFactory: PlayerFactory, private val playerFactory: PlayerFactory,
private val datePlayedService: DatePlayedService, private val datePlayedService: DatePlayedService,

View file

@ -7,6 +7,7 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.data.model.TrackIndex
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.onMain import com.github.damontecres.wholphin.ui.onMain
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
@ -41,7 +42,15 @@ sealed interface SubtitleSearchStatus {
/** /**
* Trigger a search for subtitles in the given language for the currently playing media * Trigger a search for subtitles in the given language for the currently playing media
*/ */
fun PlaybackViewModel.searchForSubtitles(language: String = Locale.current.language) { fun PlaybackViewModel.searchForSubtitles(
language: String =
serverRepository
.currentUserDto
?.configuration
?.subtitleLanguagePreference
?.takeIf { it.isNotNullOrBlank() }
?: Locale.current.language,
) {
subtitleSearchState.update { subtitleSearchState.update {
it.copy( it.copy(
status = SubtitleSearchStatus.Searching, status = SubtitleSearchStatus.Searching,