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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
|
|
@ -71,10 +71,11 @@ class SeerrServerRepository
|
|||
}
|
||||
|
||||
fun error(
|
||||
serverUrl: String,
|
||||
server: SeerrServer,
|
||||
user: SeerrUser,
|
||||
exception: Exception,
|
||||
) {
|
||||
_connection.update { SeerrConnectionStatus.Error(serverUrl, exception) }
|
||||
_connection.update { SeerrConnectionStatus.Error(server, user, exception) }
|
||||
seerrApi.update("", null)
|
||||
}
|
||||
|
||||
|
|
@ -175,8 +176,13 @@ class SeerrServerRepository
|
|||
}
|
||||
|
||||
suspend fun removeServerForCurrentUser(): Boolean {
|
||||
val current = current.firstOrNull() ?: return false
|
||||
val rows = seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
|
||||
val user =
|
||||
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()
|
||||
return rows > 0
|
||||
}
|
||||
|
|
@ -191,7 +197,8 @@ sealed interface SeerrConnectionStatus {
|
|||
data object NotConfigured : SeerrConnectionStatus
|
||||
|
||||
data class Error(
|
||||
val serverUrl: String,
|
||||
val server: SeerrServer,
|
||||
val user: SeerrUser,
|
||||
val ex: Exception,
|
||||
) : SeerrConnectionStatus
|
||||
|
||||
|
|
@ -317,7 +324,7 @@ class UserSwitchListener
|
|||
"Error logging into %s",
|
||||
server.url,
|
||||
)
|
||||
seerrServerRepository.error(server.url, ex)
|
||||
seerrServerRepository.error(server, seerrUser, ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -771,6 +771,9 @@ fun CollectionFolderGrid(
|
|||
onClickDelete = {
|
||||
showDeleteDialog = PositionItem(position, item)
|
||||
},
|
||||
onClickGoTo = {
|
||||
onClickItem.invoke(position, it)
|
||||
},
|
||||
),
|
||||
),
|
||||
onDismissRequest = { moreDialog.makeAbsent() },
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ fun CollectionFolderPhotoAlbum(
|
|||
index = index,
|
||||
filter = CollectionFolderFilter(filter = viewModel.filter.value ?: GetItemsFilter()),
|
||||
sortAndDirection = viewModel.sortAndDirection.value ?: SortAndDirection.DEFAULT,
|
||||
recursive = true,
|
||||
recursive = recursive,
|
||||
startSlideshow = false,
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ data class MoreDialogActions(
|
|||
val onClickAddPlaylist: (UUID) -> Unit,
|
||||
val onSendMediaInfo: (UUID) -> Unit,
|
||||
val onClickDelete: (BaseItem) -> Unit,
|
||||
val onClickGoTo: (BaseItem) -> Unit = { navigateTo(it.destination()) },
|
||||
)
|
||||
|
||||
enum class ClearChosenStreams {
|
||||
|
|
@ -246,7 +247,7 @@ fun buildMoreDialogItemsForHome(
|
|||
context.getString(R.string.go_to),
|
||||
Icons.Default.ArrowForward,
|
||||
) {
|
||||
actions.navigateTo(item.destination())
|
||||
actions.onClickGoTo(item)
|
||||
},
|
||||
)
|
||||
if (item.type in supportedPlayableTypes) {
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ fun DestinationContent(
|
|||
CollectionFolderPhotoAlbum(
|
||||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
recursive = true,
|
||||
recursive = false,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -387,10 +387,19 @@ fun CollectionFolder(
|
|||
}
|
||||
|
||||
CollectionType.HOMEVIDEOS,
|
||||
CollectionType.PHOTOS,
|
||||
-> {
|
||||
CollectionFolderPhotoAlbum(
|
||||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
recursive = recursiveOverride ?: false,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
CollectionType.MUSICVIDEOS,
|
||||
CollectionType.MUSIC,
|
||||
CollectionType.BOOKS,
|
||||
CollectionType.PHOTOS,
|
||||
-> {
|
||||
CollectionFolderGeneric(
|
||||
preferences,
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ fun PreferencesContent(
|
|||
when (val conn = seerrConnection) {
|
||||
is SeerrConnectionStatus.Error -> {
|
||||
SeerrDialogMode.Error(
|
||||
conn.serverUrl,
|
||||
conn.server.url,
|
||||
conn.ex,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.annotation.ExperimentalCoilApi
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.useExistingImageAsPlaceholder
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import coil3.request.transitionFactory
|
||||
import coil3.size.Size
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.VideoFilter
|
||||
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.LoadingPage
|
||||
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 timber.log.Timber
|
||||
import kotlin.math.abs
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private const val TAG = "ImagePage"
|
||||
private const val DEBUG = false
|
||||
|
||||
@SuppressLint("ConfigurationScreenWidthHeight")
|
||||
@OptIn(UnstableApi::class)
|
||||
@kotlin.OptIn(ExperimentalCoilApi::class)
|
||||
@Composable
|
||||
fun SlideshowPage(
|
||||
slideshow: Destination.Slideshow,
|
||||
|
|
@ -93,7 +97,7 @@ fun SlideshowPage(
|
|||
val imageFilter by viewModel.imageFilter.observeAsState(VideoFilter())
|
||||
val position by viewModel.position.observeAsState(0)
|
||||
val pager by viewModel.pager.observeAsState()
|
||||
val imageState by viewModel.image.observeAsState()
|
||||
// val imageState by viewModel.image.observeAsState()
|
||||
|
||||
var zoomFactor by rememberSaveable { mutableFloatStateOf(1f) }
|
||||
val isZoomed = zoomFactor * 100 > 102
|
||||
|
|
@ -197,9 +201,6 @@ fun SlideshowPage(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(imageState) {
|
||||
reset(true)
|
||||
}
|
||||
val player = viewModel.player
|
||||
val presentationState = rememberPresentationState(player)
|
||||
LaunchedEffect(slideshowActive) {
|
||||
|
|
@ -209,16 +210,6 @@ fun SlideshowPage(
|
|||
|
||||
var longPressing by remember { mutableStateOf(false) }
|
||||
|
||||
val contentModifier =
|
||||
Modifier
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
onClick = {
|
||||
showOverlay = !showOverlay
|
||||
},
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
|
|
@ -303,17 +294,20 @@ fun SlideshowPage(
|
|||
result
|
||||
},
|
||||
) {
|
||||
when (loadingState) {
|
||||
when (val st = loadingState) {
|
||||
ImageLoadingState.Error -> {
|
||||
ErrorMessage("Error loading image", null, modifier)
|
||||
}
|
||||
|
||||
ImageLoadingState.Loading -> {
|
||||
LoadingPage(modifier)
|
||||
LoadingPage(modifier, false)
|
||||
}
|
||||
|
||||
is ImageLoadingState.Success -> {
|
||||
imageState?.let { imageState ->
|
||||
val imageState = st.image
|
||||
LaunchedEffect(imageState) {
|
||||
reset(true)
|
||||
}
|
||||
if (imageState.image.data.mediaType == MediaType.VIDEO) {
|
||||
LaunchedEffect(imageState.id) {
|
||||
val mediaItem =
|
||||
|
|
@ -339,7 +333,7 @@ fun SlideshowPage(
|
|||
}
|
||||
val contentScale = ContentScale.Fit
|
||||
val scaledModifier =
|
||||
contentModifier.resizeWithContentScale(
|
||||
Modifier.resizeWithContentScale(
|
||||
contentScale,
|
||||
presentationState.videoSizeDp,
|
||||
)
|
||||
|
|
@ -377,7 +371,7 @@ fun SlideshowPage(
|
|||
val showLoadingThumbnail = true
|
||||
SubcomposeAsyncImage(
|
||||
modifier =
|
||||
contentModifier
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = zoomAnimation
|
||||
|
|
@ -402,7 +396,8 @@ fun SlideshowPage(
|
|||
.Builder(LocalContext.current)
|
||||
.data(imageState.url)
|
||||
.size(Size.ORIGINAL)
|
||||
.crossfade(!showLoadingThumbnail)
|
||||
.transitionFactory(CrossFadeFactory(750.milliseconds))
|
||||
.useExistingImageAsPlaceholder(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
|
|
@ -416,14 +411,6 @@ fun SlideshowPage(
|
|||
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
|
||||
onLoading = {
|
||||
viewModel.pulseSlideshow(Long.MAX_VALUE)
|
||||
|
|
@ -448,17 +435,22 @@ fun SlideshowPage(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
showOverlay,
|
||||
enter = slideInVertically { it },
|
||||
exit = slideOutVertically { it },
|
||||
modifier = Modifier.align(Alignment.BottomStart),
|
||||
) {
|
||||
imageState?.let { imageState ->
|
||||
when (val st = loadingState) {
|
||||
ImageLoadingState.Error -> {}
|
||||
|
||||
ImageLoadingState.Loading -> {}
|
||||
|
||||
is ImageLoadingState.Success -> {
|
||||
val imageState = st.image
|
||||
ImageOverlay(
|
||||
modifier =
|
||||
contentModifier
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.TransparentBlack50),
|
||||
onDismiss = { showOverlay = false },
|
||||
|
|
@ -481,6 +473,7 @@ fun SlideshowPage(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(showFilterDialog) {
|
||||
ImageFilterDialog(
|
||||
filter = imageFilter,
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ class SlideshowViewModel
|
|||
parentId = slideshowSettings.parentId,
|
||||
includeItemTypes = includeItemTypes,
|
||||
fields = PhotoItemFields,
|
||||
recursive = true,
|
||||
recursive = slideshowSettings.recursive,
|
||||
sortBy = listOf(slideshowSettings.sortAndDirection.sort),
|
||||
sortOrder = listOf(slideshowSettings.sortAndDirection.direction),
|
||||
),
|
||||
|
|
@ -193,7 +193,8 @@ class SlideshowViewModel
|
|||
_pager.value?.let { pager ->
|
||||
viewModelScope.launchIO {
|
||||
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}")
|
||||
if (image != null) {
|
||||
this@SlideshowViewModel.position.setValueOnMain(position)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue