mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Photo/slideshow fixes & Seerr server removal fix (#1076)
## Description A few fixes - Better crossfade for photo slideshow - Fix "Go to" in context menu for photos - Fix certain home video/photo libraries not working for photos - Fix removing seerr server in some cases ### Related issues Closes #835 Fixes #1069 Addresses https://github.com/damontecres/Wholphin/issues/634#issuecomment-4018580518 ### Testing Emulator mostly ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
afc47f254b
commit
7bb47bf329
8 changed files with 198 additions and 184 deletions
|
|
@ -25,7 +25,7 @@ import dagger.hilt.android.scopes.ActivityScoped
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
|
@ -71,10 +71,11 @@ class SeerrServerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
fun error(
|
fun error(
|
||||||
serverUrl: String,
|
server: SeerrServer,
|
||||||
|
user: SeerrUser,
|
||||||
exception: Exception,
|
exception: Exception,
|
||||||
) {
|
) {
|
||||||
_connection.update { SeerrConnectionStatus.Error(serverUrl, exception) }
|
_connection.update { SeerrConnectionStatus.Error(server, user, exception) }
|
||||||
seerrApi.update("", null)
|
seerrApi.update("", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,8 +176,13 @@ class SeerrServerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeServerForCurrentUser(): Boolean {
|
suspend fun removeServerForCurrentUser(): Boolean {
|
||||||
val current = current.firstOrNull() ?: return false
|
val user =
|
||||||
val rows = seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
|
when (val conn = connection.first()) {
|
||||||
|
SeerrConnectionStatus.NotConfigured -> return false
|
||||||
|
is SeerrConnectionStatus.Error -> conn.user
|
||||||
|
is SeerrConnectionStatus.Success -> conn.current.user
|
||||||
|
}
|
||||||
|
val rows = seerrServerDao.deleteUser(user)
|
||||||
clear()
|
clear()
|
||||||
return rows > 0
|
return rows > 0
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +197,8 @@ sealed interface SeerrConnectionStatus {
|
||||||
data object NotConfigured : SeerrConnectionStatus
|
data object NotConfigured : SeerrConnectionStatus
|
||||||
|
|
||||||
data class Error(
|
data class Error(
|
||||||
val serverUrl: String,
|
val server: SeerrServer,
|
||||||
|
val user: SeerrUser,
|
||||||
val ex: Exception,
|
val ex: Exception,
|
||||||
) : SeerrConnectionStatus
|
) : SeerrConnectionStatus
|
||||||
|
|
||||||
|
|
@ -317,7 +324,7 @@ class UserSwitchListener
|
||||||
"Error logging into %s",
|
"Error logging into %s",
|
||||||
server.url,
|
server.url,
|
||||||
)
|
)
|
||||||
seerrServerRepository.error(server.url, ex)
|
seerrServerRepository.error(server, seerrUser, ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -771,6 +771,9 @@ fun CollectionFolderGrid(
|
||||||
onClickDelete = {
|
onClickDelete = {
|
||||||
showDeleteDialog = PositionItem(position, item)
|
showDeleteDialog = PositionItem(position, item)
|
||||||
},
|
},
|
||||||
|
onClickGoTo = {
|
||||||
|
onClickItem.invoke(position, it)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onDismissRequest = { moreDialog.makeAbsent() },
|
onDismissRequest = { moreDialog.makeAbsent() },
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ fun CollectionFolderPhotoAlbum(
|
||||||
index = index,
|
index = index,
|
||||||
filter = CollectionFolderFilter(filter = viewModel.filter.value ?: GetItemsFilter()),
|
filter = CollectionFolderFilter(filter = viewModel.filter.value ?: GetItemsFilter()),
|
||||||
sortAndDirection = viewModel.sortAndDirection.value ?: SortAndDirection.DEFAULT,
|
sortAndDirection = viewModel.sortAndDirection.value ?: SortAndDirection.DEFAULT,
|
||||||
recursive = true,
|
recursive = recursive,
|
||||||
startSlideshow = false,
|
startSlideshow = false,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ data class MoreDialogActions(
|
||||||
val onClickAddPlaylist: (UUID) -> Unit,
|
val onClickAddPlaylist: (UUID) -> Unit,
|
||||||
val onSendMediaInfo: (UUID) -> Unit,
|
val onSendMediaInfo: (UUID) -> Unit,
|
||||||
val onClickDelete: (BaseItem) -> Unit,
|
val onClickDelete: (BaseItem) -> Unit,
|
||||||
|
val onClickGoTo: (BaseItem) -> Unit = { navigateTo(it.destination()) },
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ClearChosenStreams {
|
enum class ClearChosenStreams {
|
||||||
|
|
@ -246,7 +247,7 @@ fun buildMoreDialogItemsForHome(
|
||||||
context.getString(R.string.go_to),
|
context.getString(R.string.go_to),
|
||||||
Icons.Default.ArrowForward,
|
Icons.Default.ArrowForward,
|
||||||
) {
|
) {
|
||||||
actions.navigateTo(item.destination())
|
actions.onClickGoTo(item)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (item.type in supportedPlayableTypes) {
|
if (item.type in supportedPlayableTypes) {
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ fun DestinationContent(
|
||||||
CollectionFolderPhotoAlbum(
|
CollectionFolderPhotoAlbum(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
recursive = true,
|
recursive = false,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -387,10 +387,19 @@ fun CollectionFolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionType.HOMEVIDEOS,
|
CollectionType.HOMEVIDEOS,
|
||||||
|
CollectionType.PHOTOS,
|
||||||
|
-> {
|
||||||
|
CollectionFolderPhotoAlbum(
|
||||||
|
preferences = preferences,
|
||||||
|
itemId = destination.itemId,
|
||||||
|
recursive = recursiveOverride ?: false,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
CollectionType.MUSICVIDEOS,
|
CollectionType.MUSICVIDEOS,
|
||||||
CollectionType.MUSIC,
|
CollectionType.MUSIC,
|
||||||
CollectionType.BOOKS,
|
CollectionType.BOOKS,
|
||||||
CollectionType.PHOTOS,
|
|
||||||
-> {
|
-> {
|
||||||
CollectionFolderGeneric(
|
CollectionFolderGeneric(
|
||||||
preferences,
|
preferences,
|
||||||
|
|
|
||||||
|
|
@ -400,7 +400,7 @@ fun PreferencesContent(
|
||||||
when (val conn = seerrConnection) {
|
when (val conn = seerrConnection) {
|
||||||
is SeerrConnectionStatus.Error -> {
|
is SeerrConnectionStatus.Error -> {
|
||||||
SeerrDialogMode.Error(
|
SeerrDialogMode.Error(
|
||||||
conn.serverUrl,
|
conn.server.url,
|
||||||
conn.ex,
|
conn.ex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.focusable
|
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
|
||||||
|
|
@ -53,13 +52,16 @@ import androidx.media3.ui.compose.modifiers.resizeWithContentScale
|
||||||
import androidx.media3.ui.compose.state.rememberPresentationState
|
import androidx.media3.ui.compose.state.rememberPresentationState
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import coil3.annotation.ExperimentalCoilApi
|
||||||
import coil3.compose.SubcomposeAsyncImage
|
import coil3.compose.SubcomposeAsyncImage
|
||||||
|
import coil3.compose.useExistingImageAsPlaceholder
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.crossfade
|
import coil3.request.transitionFactory
|
||||||
import coil3.size.Size
|
import coil3.size.Size
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.VideoFilter
|
import com.github.damontecres.wholphin.data.model.VideoFilter
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
|
import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -70,12 +72,14 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
private const val TAG = "ImagePage"
|
private const val TAG = "ImagePage"
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
|
|
||||||
@SuppressLint("ConfigurationScreenWidthHeight")
|
@SuppressLint("ConfigurationScreenWidthHeight")
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
|
@kotlin.OptIn(ExperimentalCoilApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SlideshowPage(
|
fun SlideshowPage(
|
||||||
slideshow: Destination.Slideshow,
|
slideshow: Destination.Slideshow,
|
||||||
|
|
@ -93,7 +97,7 @@ fun SlideshowPage(
|
||||||
val imageFilter by viewModel.imageFilter.observeAsState(VideoFilter())
|
val imageFilter by viewModel.imageFilter.observeAsState(VideoFilter())
|
||||||
val position by viewModel.position.observeAsState(0)
|
val position by viewModel.position.observeAsState(0)
|
||||||
val pager by viewModel.pager.observeAsState()
|
val pager by viewModel.pager.observeAsState()
|
||||||
val imageState by viewModel.image.observeAsState()
|
// val imageState by viewModel.image.observeAsState()
|
||||||
|
|
||||||
var zoomFactor by rememberSaveable { mutableFloatStateOf(1f) }
|
var zoomFactor by rememberSaveable { mutableFloatStateOf(1f) }
|
||||||
val isZoomed = zoomFactor * 100 > 102
|
val isZoomed = zoomFactor * 100 > 102
|
||||||
|
|
@ -197,9 +201,6 @@ fun SlideshowPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(imageState) {
|
|
||||||
reset(true)
|
|
||||||
}
|
|
||||||
val player = viewModel.player
|
val player = viewModel.player
|
||||||
val presentationState = rememberPresentationState(player)
|
val presentationState = rememberPresentationState(player)
|
||||||
LaunchedEffect(slideshowActive) {
|
LaunchedEffect(slideshowActive) {
|
||||||
|
|
@ -209,16 +210,6 @@ fun SlideshowPage(
|
||||||
|
|
||||||
var longPressing by remember { mutableStateOf(false) }
|
var longPressing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val contentModifier =
|
|
||||||
Modifier
|
|
||||||
.clickable(
|
|
||||||
interactionSource = null,
|
|
||||||
indication = null,
|
|
||||||
onClick = {
|
|
||||||
showOverlay = !showOverlay
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
|
|
@ -303,17 +294,20 @@ fun SlideshowPage(
|
||||||
result
|
result
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
when (loadingState) {
|
when (val st = loadingState) {
|
||||||
ImageLoadingState.Error -> {
|
ImageLoadingState.Error -> {
|
||||||
ErrorMessage("Error loading image", null, modifier)
|
ErrorMessage("Error loading image", null, modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageLoadingState.Loading -> {
|
ImageLoadingState.Loading -> {
|
||||||
LoadingPage(modifier)
|
LoadingPage(modifier, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
is ImageLoadingState.Success -> {
|
is ImageLoadingState.Success -> {
|
||||||
imageState?.let { imageState ->
|
val imageState = st.image
|
||||||
|
LaunchedEffect(imageState) {
|
||||||
|
reset(true)
|
||||||
|
}
|
||||||
if (imageState.image.data.mediaType == MediaType.VIDEO) {
|
if (imageState.image.data.mediaType == MediaType.VIDEO) {
|
||||||
LaunchedEffect(imageState.id) {
|
LaunchedEffect(imageState.id) {
|
||||||
val mediaItem =
|
val mediaItem =
|
||||||
|
|
@ -339,7 +333,7 @@ fun SlideshowPage(
|
||||||
}
|
}
|
||||||
val contentScale = ContentScale.Fit
|
val contentScale = ContentScale.Fit
|
||||||
val scaledModifier =
|
val scaledModifier =
|
||||||
contentModifier.resizeWithContentScale(
|
Modifier.resizeWithContentScale(
|
||||||
contentScale,
|
contentScale,
|
||||||
presentationState.videoSizeDp,
|
presentationState.videoSizeDp,
|
||||||
)
|
)
|
||||||
|
|
@ -377,7 +371,7 @@ fun SlideshowPage(
|
||||||
val showLoadingThumbnail = true
|
val showLoadingThumbnail = true
|
||||||
SubcomposeAsyncImage(
|
SubcomposeAsyncImage(
|
||||||
modifier =
|
modifier =
|
||||||
contentModifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = zoomAnimation
|
scaleX = zoomAnimation
|
||||||
|
|
@ -402,7 +396,8 @@ fun SlideshowPage(
|
||||||
.Builder(LocalContext.current)
|
.Builder(LocalContext.current)
|
||||||
.data(imageState.url)
|
.data(imageState.url)
|
||||||
.size(Size.ORIGINAL)
|
.size(Size.ORIGINAL)
|
||||||
.crossfade(!showLoadingThumbnail)
|
.transitionFactory(CrossFadeFactory(750.milliseconds))
|
||||||
|
.useExistingImageAsPlaceholder(true)
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
|
|
@ -416,14 +411,6 @@ fun SlideshowPage(
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
loading = {
|
|
||||||
ImageLoadingPlaceholder(
|
|
||||||
thumbnailUrl = imageState.thumbnailUrl,
|
|
||||||
showThumbnail = showLoadingThumbnail,
|
|
||||||
colorFilter = colorFilter,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
// Ensure that if an image takes a long time to load, it won't be skipped
|
// Ensure that if an image takes a long time to load, it won't be skipped
|
||||||
onLoading = {
|
onLoading = {
|
||||||
viewModel.pulseSlideshow(Long.MAX_VALUE)
|
viewModel.pulseSlideshow(Long.MAX_VALUE)
|
||||||
|
|
@ -448,17 +435,22 @@ fun SlideshowPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
showOverlay,
|
showOverlay,
|
||||||
enter = slideInVertically { it },
|
enter = slideInVertically { it },
|
||||||
exit = slideOutVertically { it },
|
exit = slideOutVertically { it },
|
||||||
modifier = Modifier.align(Alignment.BottomStart),
|
modifier = Modifier.align(Alignment.BottomStart),
|
||||||
) {
|
) {
|
||||||
imageState?.let { imageState ->
|
when (val st = loadingState) {
|
||||||
|
ImageLoadingState.Error -> {}
|
||||||
|
|
||||||
|
ImageLoadingState.Loading -> {}
|
||||||
|
|
||||||
|
is ImageLoadingState.Success -> {
|
||||||
|
val imageState = st.image
|
||||||
ImageOverlay(
|
ImageOverlay(
|
||||||
modifier =
|
modifier =
|
||||||
contentModifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(AppColors.TransparentBlack50),
|
.background(AppColors.TransparentBlack50),
|
||||||
onDismiss = { showOverlay = false },
|
onDismiss = { showOverlay = false },
|
||||||
|
|
@ -481,6 +473,7 @@ fun SlideshowPage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
AnimatedVisibility(showFilterDialog) {
|
AnimatedVisibility(showFilterDialog) {
|
||||||
ImageFilterDialog(
|
ImageFilterDialog(
|
||||||
filter = imageFilter,
|
filter = imageFilter,
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ class SlideshowViewModel
|
||||||
parentId = slideshowSettings.parentId,
|
parentId = slideshowSettings.parentId,
|
||||||
includeItemTypes = includeItemTypes,
|
includeItemTypes = includeItemTypes,
|
||||||
fields = PhotoItemFields,
|
fields = PhotoItemFields,
|
||||||
recursive = true,
|
recursive = slideshowSettings.recursive,
|
||||||
sortBy = listOf(slideshowSettings.sortAndDirection.sort),
|
sortBy = listOf(slideshowSettings.sortAndDirection.sort),
|
||||||
sortOrder = listOf(slideshowSettings.sortAndDirection.direction),
|
sortOrder = listOf(slideshowSettings.sortAndDirection.direction),
|
||||||
),
|
),
|
||||||
|
|
@ -193,7 +193,8 @@ class SlideshowViewModel
|
||||||
_pager.value?.let { pager ->
|
_pager.value?.let { pager ->
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
try {
|
try {
|
||||||
val image = pager.getBlocking(position)
|
val image =
|
||||||
|
if (position in pager.indices) pager.getBlocking(position) else null
|
||||||
Timber.v("Got image for $position: ${image != null}")
|
Timber.v("Got image for $position: ${image != null}")
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
this@SlideshowViewModel.position.setValueOnMain(position)
|
this@SlideshowViewModel.position.setValueOnMain(position)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue