Fix toasting & some UI changes

This commit is contained in:
Damontecres 2025-10-12 17:28:34 -04:00
parent cbc680da7d
commit 29e13cba0f
No known key found for this signature in database
14 changed files with 137 additions and 77 deletions

View file

@ -33,6 +33,7 @@ import com.github.damontecres.dolphin.ui.nav.ApplicationContent
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.theme.DolphinTheme import com.github.damontecres.dolphin.ui.theme.DolphinTheme
import com.github.damontecres.dolphin.util.UpdateChecker
import com.github.damontecres.dolphin.util.profile.createDeviceProfile import com.github.damontecres.dolphin.util.profile.createDeviceProfile
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -54,6 +55,9 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var navigationManager: NavigationManager lateinit var navigationManager: NavigationManager
@Inject
lateinit var updateChecker: UpdateChecker
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -122,6 +126,15 @@ class MainActivity : AppCompatActivity() {
} }
val backStack = rememberNavBackStack(initialDestination) val backStack = rememberNavBackStack(initialDestination)
navigationManager.backStack = backStack navigationManager.backStack = backStack
if (appPreferences.autoCheckForUpdates) {
LaunchedEffect(Unit) {
try {
updateChecker.maybeShowUpdateToast(appPreferences.updateUrl)
} catch (ex: Exception) {
Timber.w(ex, "Failed to check for update")
}
}
}
ApplicationContent( ApplicationContent(
user = user, user = user,
server = server, server = server,

View file

@ -6,6 +6,7 @@ import android.content.ContextWrapper
import android.media.AudioManager import android.media.AudioManager
import android.view.KeyEvent import android.view.KeyEvent
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast
import androidx.compose.foundation.MarqueeAnimationMode import androidx.compose.foundation.MarqueeAnimationMode
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -29,6 +30,8 @@ import coil3.request.ErrorResult
import com.github.damontecres.dolphin.ui.data.RowColumn import com.github.damontecres.dolphin.ui.data.RowColumn
import com.github.damontecres.dolphin.ui.data.RowColumnSaver import com.github.damontecres.dolphin.ui.data.RowColumnSaver
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber import timber.log.Timber
@ -309,3 +312,21 @@ fun rememberPosition(initialPosition: RowColumn = RowColumn(-1, -1)) =
initialPosition, initialPosition,
) )
} }
/**
* Show a [Toast]. Ensures it runs on the main thread.
*/
suspend fun showToast(
context: Context,
text: CharSequence,
duration: Int,
) = withContext(Dispatchers.Main) {
Toast.makeText(context, text, duration).show()
}
suspend fun showToast(
context: Context,
text: CharSequence,
) = withContext(Dispatchers.Main) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}

View file

@ -1,14 +1,20 @@
package com.github.damontecres.dolphin.ui.components package com.github.damontecres.dolphin.ui.components
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import com.github.damontecres.dolphin.ui.tryRequestFocus
@Composable @Composable
fun CircularProgress(modifier: Modifier = Modifier) { fun CircularProgress(modifier: Modifier = Modifier) {
@ -21,13 +27,19 @@ fun CircularProgress(modifier: Modifier = Modifier) {
} }
/** /**
* Fill the space with a loading indicator * Fill the space with a loading indicator and take focus
*/ */
@Composable @Composable
fun LoadingPage(modifier: Modifier = Modifier) { fun LoadingPage(modifier: Modifier = Modifier) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize(), modifier =
modifier
.fillMaxSize()
.focusRequester(focusRequester)
.focusable(),
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
color = MaterialTheme.colorScheme.border, color = MaterialTheme.colorScheme.border,

View file

@ -20,6 +20,7 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.letNotEmpty import com.github.damontecres.dolphin.ui.letNotEmpty
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.showToast
import com.github.damontecres.dolphin.util.ApiRequestPager import com.github.damontecres.dolphin.util.ApiRequestPager
import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
@ -297,9 +298,11 @@ class SeriesViewModel
if (nextUp != null) { if (nextUp != null) {
navigateTo(Destination.Playback(nextUp.id, 0L)) navigateTo(Destination.Playback(nextUp.id, 0L))
} else { } else {
Toast showToast(
.makeText(context, "Could not find an episode to play", Toast.LENGTH_SHORT) context,
.show() "Could not find an episode to play",
Toast.LENGTH_SHORT,
)
} }
} }
} }

View file

@ -41,7 +41,9 @@ import com.github.damontecres.dolphin.ui.playOnClickSound
import com.github.damontecres.dolphin.ui.playSoundOnFocus import com.github.damontecres.dolphin.ui.playSoundOnFocus
import com.github.damontecres.dolphin.ui.roundMinutes import com.github.damontecres.dolphin.ui.roundMinutes
import com.github.damontecres.dolphin.ui.timeRemaining import com.github.damontecres.dolphin.ui.timeRemaining
import com.github.damontecres.dolphin.util.formatSubtitleLang
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.PersonKind
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
@Composable @Composable
@ -153,6 +155,14 @@ fun MovieDetailsHeader(
) )
} }
} }
movie.data.people
?.filter { it.type == PersonKind.DIRECTOR && it.name.isNotNullOrBlank() }
?.joinToString(", ") { it.name!! }
?.let {
Text(
text = "Directed by $it",
)
}
// Key-Values // Key-Values
Row( Row(
modifier = modifier =
@ -185,10 +195,7 @@ fun MovieDetailsHeader(
) )
} }
} }
dto.mediaStreams formatSubtitleLang(dto.mediaStreams)
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
?.mapNotNull { it.language }
?.joinToString(", ")
?.let { ?.let {
if (it.isNotNullOrBlank()) { if (it.isNotNullOrBlank()) {
TitleValueText( TitleValueText(
@ -199,13 +206,6 @@ fun MovieDetailsHeader(
} }
} }
// TODO add writers, studio, etc to overview dialog // TODO add writers, studio, etc to overview dialog
// dto.people?.firstOrNull { it.type == PersonKind.DIRECTOR }?.name?.let {
// TitleValueText(
// stringResource(R.string.director),
// it,
// modifier = Modifier.widthIn(max = 80.dp),
// )
// }
// dto.studios?.letNotEmpty { // dto.studios?.letNotEmpty {
// TitleValueText( // TitleValueText(
// stringResource(R.string.studios), // stringResource(R.string.studios),

View file

@ -15,6 +15,7 @@ import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.dolphin.ui.components.TitleValueText import com.github.damontecres.dolphin.ui.components.TitleValueText
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.util.formatSubtitleLang
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration import kotlin.time.Duration
@ -75,10 +76,7 @@ fun FocusedEpisodeFooter(
} }
} }
dto.mediaStreams formatSubtitleLang(dto.mediaStreams)
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
?.mapNotNull { it.language }
?.joinToString(", ")
?.let { ?.let {
if (it.isNotNullOrBlank()) { if (it.isNotNullOrBlank()) {
TitleValueText( TitleValueText(

View file

@ -193,11 +193,7 @@ class HomeViewModel
.filter { it.collectionType in supportedCollectionTypes } .filter { it.collectionType in supportedCollectionTypes }
.map { view -> .map { view ->
val title = val title =
view view.name?.let { "Recently Added in $it" }
?.name
?.let {
"Recently Added in $it"
}
val request = val request =
GetLatestMediaRequest( GetLatestMediaRequest(
fields = DefaultItemFields, fields = DefaultItemFields,
@ -205,6 +201,7 @@ class HomeViewModel
parentId = view.id, parentId = view.id,
groupItems = true, groupItems = true,
limit = limit, limit = limit,
isPlayed = null, // Server will handle user's preference
) )
val latest = val latest =
api.userLibraryApi api.userLibraryApi

View file

@ -66,11 +66,14 @@ import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.supportedCollectionTypes import com.github.damontecres.dolphin.util.supportedCollectionTypes
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.api.client.extensions.userViewsApi
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 javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -85,15 +88,19 @@ class NavDrawerViewModel
val selectedIndex = MutableLiveData<Int>(-1) val selectedIndex = MutableLiveData<Int>(-1)
init { init {
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launch(Dispatchers.IO + ExceptionHandler(true)) {
val userViews = val userViews =
api.userViewsApi api.userViewsApi
.getUserViews() .getUserViews()
.content.items .content.items
libraries.value = val libraries =
userViews userViews
.filter { it.collectionType in supportedCollectionTypes } .filter { it.collectionType in supportedCollectionTypes }
.map { BaseItem.from(it, api) } .map { BaseItem.from(it, api) }
Timber.d("Got ${userViews.size} user views filtered to ${libraries.size}")
withContext(Dispatchers.Main) {
this@NavDrawerViewModel.libraries.value = libraries
}
} }
} }

View file

@ -264,22 +264,22 @@ fun PlaybackOverlay(
} }
} }
} }
AnimatedVisibility( }
showDebugInfo && controllerViewState.controlsVisible, AnimatedVisibility(
modifier = showDebugInfo && controllerViewState.controlsVisible,
Modifier modifier =
.align(Alignment.TopStart), Modifier
) { .align(Alignment.TopStart),
currentPlayback?.tracks?.letNotEmpty { ) {
PlaybackTrackInfo( currentPlayback?.tracks?.letNotEmpty {
trackSupport = it, PlaybackTrackInfo(
modifier = trackSupport = it,
Modifier modifier =
.align(Alignment.TopStart) Modifier
.padding(16.dp) .align(Alignment.TopStart)
.background(AppColors.TransparentBlack50), .padding(16.dp)
) .background(AppColors.TransparentBlack50),
} )
} }
} }
} }

View file

@ -2,8 +2,10 @@ package com.github.damontecres.dolphin.util
import android.widget.Toast import android.widget.Toast
import com.github.damontecres.dolphin.DolphinApplication import com.github.damontecres.dolphin.DolphinApplication
import com.github.damontecres.dolphin.ui.showToast
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -29,12 +31,13 @@ class ExceptionHandler(
Timber.e(exception, "Exception in coroutine") Timber.e(exception, "Exception in coroutine")
if (autoToast) { if (autoToast) {
Toast runBlocking {
.makeText( showToast(
DolphinApplication.instance, DolphinApplication.instance,
"Error: ${exception.message}", "Error: ${exception.message}",
Toast.LENGTH_LONG, Toast.LENGTH_LONG,
).show() )
}
} }
} }
} }

View file

@ -1,7 +1,10 @@
package com.github.damontecres.dolphin.util package com.github.damontecres.dolphin.util
import android.os.Build import android.os.Build
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.MediaStreamType
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -48,3 +51,10 @@ val BaseItemDto.seasonEpisodePadded: String?
} else { } else {
null null
} }
fun formatSubtitleLang(mediaStreams: List<MediaStream>?): String? =
mediaStreams
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
?.mapNotNull { it.language }
?.distinct()
?.joinToString(", ") { languageName(it) }

View file

@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.util
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.github.damontecres.dolphin.DolphinApplication import com.github.damontecres.dolphin.DolphinApplication
import com.github.damontecres.dolphin.ui.showToast
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -39,15 +40,13 @@ class LoadingExceptionHandler(
exception = exception, exception = exception,
) )
} }
} if (autoToast) {
showToast(
if (autoToast) {
Toast
.makeText(
DolphinApplication.instance, DolphinApplication.instance,
"Error: ${exception.message}", "Error: ${exception.message}",
Toast.LENGTH_LONG, Toast.LENGTH_LONG,
).show() )
}
} }
} }
} }

View file

@ -43,7 +43,7 @@ data class TrackSupport(
if (split.size > 1) split[1] else codecs if (split.size > 1) split[1] else codecs
} }
} }
val language = languageName(context, format.language) val language = languageName(format.language)
"$language ($type)" "$language ($type)"
} }
} }
@ -135,16 +135,14 @@ fun checkForSupport(tracks: Tracks): List<TrackSupport> =
} }
} }
fun languageName( fun languageName(code: String?): String =
context: Context, if (code != null) {
code: String?, try {
) = if (code != null && code != "00") { Locale(code).displayLanguage
try { } catch (ex: Exception) {
Locale(code).displayLanguage Timber.w(ex, "Error in locale for '$code'")
} catch (ex: Exception) { code.uppercase()
Timber.w(ex, "Error in locale for '$code'") }
code.uppercase() } else {
"Unknown"
} }
} else {
"Unknown"
}

View file

@ -19,6 +19,7 @@ import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.hilt.StandardOkHttpClient import com.github.damontecres.dolphin.hilt.StandardOkHttpClient
import com.github.damontecres.dolphin.ui.findActivity import com.github.damontecres.dolphin.ui.findActivity
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.showToast
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -87,22 +88,20 @@ class UpdateChecker
"Skipping update notification, threshold is $lastUpdateCheckThreshold", "Skipping update notification, threshold is $lastUpdateCheckThreshold",
) )
} else { } else {
Toast showToast(
.makeText( context,
context, "Update available: $installedVersion => ${latestRelease.version}!",
"Update available: $installedVersion => ${latestRelease.version}!", Toast.LENGTH_LONG,
Toast.LENGTH_LONG, )
).show()
} }
} else { } else {
Timber.v("No update available for $installedVersion") Timber.v("No update available for $installedVersion")
if (showNegativeToast) { if (showNegativeToast) {
Toast showToast(
.makeText( context,
context, "No updates available, $installedVersion is the latest!",
"No updates available, $installedVersion is the latest!", Toast.LENGTH_LONG,
Toast.LENGTH_LONG, )
).show()
} }
} }
} }