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.NavigationManager
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
import com.github.damontecres.dolphin.util.UpdateChecker
import com.github.damontecres.dolphin.util.profile.createDeviceProfile
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.OkHttpClient
@ -54,6 +55,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var navigationManager: NavigationManager
@Inject
lateinit var updateChecker: UpdateChecker
@OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -122,6 +126,15 @@ class MainActivity : AppCompatActivity() {
}
val backStack = rememberNavBackStack(initialDestination)
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(
user = user,
server = server,

View file

@ -6,6 +6,7 @@ import android.content.ContextWrapper
import android.media.AudioManager
import android.view.KeyEvent
import android.view.WindowManager
import android.widget.Toast
import androidx.compose.foundation.MarqueeAnimationMode
import androidx.compose.foundation.basicMarquee
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.RowColumnSaver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber
@ -309,3 +312,21 @@ fun rememberPosition(initialPosition: RowColumn = RowColumn(-1, -1)) =
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
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
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.tv.material3.MaterialTheme
import com.github.damontecres.dolphin.ui.tryRequestFocus
@Composable
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
fun LoadingPage(modifier: Modifier = Modifier) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Box(
contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize(),
modifier =
modifier
.fillMaxSize()
.focusRequester(focusRequester)
.focusable(),
) {
CircularProgressIndicator(
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.nav.Destination
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.ExceptionHandler
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
@ -297,9 +298,11 @@ class SeriesViewModel
if (nextUp != null) {
navigateTo(Destination.Playback(nextUp.id, 0L))
} else {
Toast
.makeText(context, "Could not find an episode to play", Toast.LENGTH_SHORT)
.show()
showToast(
context,
"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.roundMinutes
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.PersonKind
import org.jellyfin.sdk.model.extensions.ticks
@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
Row(
modifier =
@ -185,10 +195,7 @@ fun MovieDetailsHeader(
)
}
}
dto.mediaStreams
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
?.mapNotNull { it.language }
?.joinToString(", ")
formatSubtitleLang(dto.mediaStreams)
?.let {
if (it.isNotNullOrBlank()) {
TitleValueText(
@ -199,13 +206,6 @@ fun MovieDetailsHeader(
}
}
// 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 {
// TitleValueText(
// 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.TitleValueText
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.extensions.ticks
import kotlin.time.Duration
@ -75,10 +76,7 @@ fun FocusedEpisodeFooter(
}
}
dto.mediaStreams
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
?.mapNotNull { it.language }
?.joinToString(", ")
formatSubtitleLang(dto.mediaStreams)
?.let {
if (it.isNotNullOrBlank()) {
TitleValueText(

View file

@ -193,11 +193,7 @@ class HomeViewModel
.filter { it.collectionType in supportedCollectionTypes }
.map { view ->
val title =
view
?.name
?.let {
"Recently Added in $it"
}
view.name?.let { "Recently Added in $it" }
val request =
GetLatestMediaRequest(
fields = DefaultItemFields,
@ -205,6 +201,7 @@ class HomeViewModel
parentId = view.id,
groupItems = true,
limit = limit,
isPlayed = null, // Server will handle user's preference
)
val latest =
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.supportedCollectionTypes
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@ -85,15 +88,19 @@ class NavDrawerViewModel
val selectedIndex = MutableLiveData<Int>(-1)
init {
viewModelScope.launch(ExceptionHandler()) {
viewModelScope.launch(Dispatchers.IO + ExceptionHandler(true)) {
val userViews =
api.userViewsApi
.getUserViews()
.content.items
libraries.value =
val libraries =
userViews
.filter { it.collectionType in supportedCollectionTypes }
.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,
modifier =
Modifier
.align(Alignment.TopStart),
) {
currentPlayback?.tracks?.letNotEmpty {
PlaybackTrackInfo(
trackSupport = it,
modifier =
Modifier
.align(Alignment.TopStart)
.padding(16.dp)
.background(AppColors.TransparentBlack50),
)
}
}
AnimatedVisibility(
showDebugInfo && controllerViewState.controlsVisible,
modifier =
Modifier
.align(Alignment.TopStart),
) {
currentPlayback?.tracks?.letNotEmpty {
PlaybackTrackInfo(
trackSupport = it,
modifier =
Modifier
.align(Alignment.TopStart)
.padding(16.dp)
.background(AppColors.TransparentBlack50),
)
}
}
}

View file

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

View file

@ -1,7 +1,10 @@
package com.github.damontecres.dolphin.util
import android.os.Build
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
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.format.DateTimeFormatter
@ -48,3 +51,10 @@ val BaseItemDto.seasonEpisodePadded: String?
} else {
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 androidx.lifecycle.MutableLiveData
import com.github.damontecres.dolphin.DolphinApplication
import com.github.damontecres.dolphin.ui.showToast
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
@ -39,15 +40,13 @@ class LoadingExceptionHandler(
exception = exception,
)
}
}
if (autoToast) {
Toast
.makeText(
if (autoToast) {
showToast(
DolphinApplication.instance,
"Error: ${exception.message}",
Toast.LENGTH_LONG,
).show()
)
}
}
}
}

View file

@ -43,7 +43,7 @@ data class TrackSupport(
if (split.size > 1) split[1] else codecs
}
}
val language = languageName(context, format.language)
val language = languageName(format.language)
"$language ($type)"
}
}
@ -135,16 +135,14 @@ fun checkForSupport(tracks: Tracks): List<TrackSupport> =
}
}
fun languageName(
context: Context,
code: String?,
) = if (code != null && code != "00") {
try {
Locale(code).displayLanguage
} catch (ex: Exception) {
Timber.w(ex, "Error in locale for '$code'")
code.uppercase()
fun languageName(code: String?): String =
if (code != null) {
try {
Locale(code).displayLanguage
} catch (ex: Exception) {
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.ui.findActivity
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.showToast
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -87,22 +88,20 @@ class UpdateChecker
"Skipping update notification, threshold is $lastUpdateCheckThreshold",
)
} else {
Toast
.makeText(
context,
"Update available: $installedVersion => ${latestRelease.version}!",
Toast.LENGTH_LONG,
).show()
showToast(
context,
"Update available: $installedVersion => ${latestRelease.version}!",
Toast.LENGTH_LONG,
)
}
} else {
Timber.v("No update available for $installedVersion")
if (showNegativeToast) {
Toast
.makeText(
context,
"No updates available, $installedVersion is the latest!",
Toast.LENGTH_LONG,
).show()
showToast(
context,
"No updates available, $installedVersion is the latest!",
Toast.LENGTH_LONG,
)
}
}
}