mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 08:39:24 +00:00
feat: implement reorderable list for collections and folders
This commit is contained in:
parent
bd4f52bcfd
commit
78cc2abb5e
5 changed files with 239 additions and 100 deletions
|
|
@ -125,18 +125,19 @@ object CollectionEditorRepository {
|
|||
}
|
||||
|
||||
fun moveFolderUp(index: Int) {
|
||||
val list = _uiState.value.folders.toMutableList()
|
||||
if (index <= 0 || index >= list.size) return
|
||||
val item = list.removeAt(index)
|
||||
list.add(index - 1, item)
|
||||
_uiState.value = _uiState.value.copy(folders = list)
|
||||
moveFolderByIndex(index, index - 1)
|
||||
}
|
||||
|
||||
fun moveFolderDown(index: Int) {
|
||||
moveFolderByIndex(index, index + 1)
|
||||
}
|
||||
|
||||
fun moveFolderByIndex(fromIndex: Int, toIndex: Int) {
|
||||
if (fromIndex == toIndex) return
|
||||
val list = _uiState.value.folders.toMutableList()
|
||||
if (index < 0 || index >= list.size - 1) return
|
||||
val item = list.removeAt(index)
|
||||
list.add(index + 1, item)
|
||||
if (fromIndex !in list.indices || toIndex !in list.indices) return
|
||||
val item = list.removeAt(fromIndex)
|
||||
list.add(toIndex, item)
|
||||
_uiState.value = _uiState.value.copy(folders = list)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,20 +13,21 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
||||
import androidx.compose.material.icons.rounded.ArrowUpward
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
|
|
@ -46,11 +47,13 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import com.nuvio.app.core.ui.NuvioInputField
|
||||
import com.nuvio.app.core.ui.NuvioPrimaryButton
|
||||
import com.nuvio.app.core.ui.NuvioScreen
|
||||
|
|
@ -58,6 +61,9 @@ import com.nuvio.app.core.ui.NuvioScreenHeader
|
|||
import com.nuvio.app.core.ui.NuvioSectionLabel
|
||||
import com.nuvio.app.core.ui.NuvioSurfaceCard
|
||||
import com.nuvio.app.features.home.PosterShape
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -289,19 +295,14 @@ fun CollectionEditorScreen(
|
|||
}
|
||||
|
||||
// Folder Items
|
||||
itemsIndexed(
|
||||
items = state.folders,
|
||||
key = { _, folder -> folder.id },
|
||||
) { index, folder ->
|
||||
FolderListItem(
|
||||
folder = folder,
|
||||
index = index,
|
||||
totalCount = state.folders.size,
|
||||
onEdit = { CollectionEditorRepository.editFolder(folder.id) },
|
||||
onDelete = { CollectionEditorRepository.removeFolder(folder.id) },
|
||||
onMoveUp = { CollectionEditorRepository.moveFolderUp(index) },
|
||||
onMoveDown = { CollectionEditorRepository.moveFolderDown(index) },
|
||||
)
|
||||
if (state.folders.isNotEmpty()) {
|
||||
item {
|
||||
FolderReorderableList(
|
||||
folders = state.folders,
|
||||
onEdit = { CollectionEditorRepository.editFolder(it) },
|
||||
onDelete = { CollectionEditorRepository.removeFolder(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.folders.isEmpty()) {
|
||||
|
|
@ -340,16 +341,58 @@ fun CollectionEditorScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FolderReorderableList(
|
||||
folders: List<CollectionFolder>,
|
||||
onEdit: (String) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyListState = rememberReorderableLazyListState(
|
||||
lazyListState = lazyListState,
|
||||
) { from, to ->
|
||||
CollectionEditorRepository.moveFolderByIndex(from.index, to.index)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 720.dp),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
itemsIndexed(folders, key = { _, folder -> folder.id }) { _, folder ->
|
||||
ReorderableItem(reorderableLazyListState, key = folder.id) { isDragging ->
|
||||
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
shadowElevation = elevation,
|
||||
) {
|
||||
FolderListItem(
|
||||
folder = folder,
|
||||
onEdit = { onEdit(folder.id) },
|
||||
onDelete = { onDelete(folder.id) },
|
||||
dragHandleScope = this@ReorderableItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FolderListItem(
|
||||
folder: CollectionFolder,
|
||||
index: Int,
|
||||
totalCount: Int,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onMoveUp: () -> Unit,
|
||||
onMoveDown: () -> Unit,
|
||||
dragHandleScope: ReorderableCollectionItemScope,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
NuvioSurfaceCard {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -385,27 +428,28 @@ private fun FolderListItem(
|
|||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onMoveUp,
|
||||
enabled = index > 0,
|
||||
modifier = Modifier.size(36.dp),
|
||||
modifier = with(dragHandleScope) {
|
||||
Modifier.draggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
},
|
||||
).size(36.dp)
|
||||
},
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ArrowUpward,
|
||||
contentDescription = "Move up",
|
||||
modifier = Modifier.size(20.dp).alpha(if (index > 0) 1f else 0.3f),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onMoveDown,
|
||||
enabled = index < totalCount - 1,
|
||||
modifier = Modifier.size(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ArrowDownward,
|
||||
contentDescription = "Move down",
|
||||
modifier = Modifier.size(20.dp).alpha(if (index < totalCount - 1) 1f else 0.3f),
|
||||
imageVector = Icons.Rounded.Menu,
|
||||
contentDescription = "Reorder",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
|
|
|||
|
|
@ -7,22 +7,23 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
||||
import androidx.compose.material.icons.rounded.ArrowUpward
|
||||
import androidx.compose.material.icons.rounded.ContentCopy
|
||||
import androidx.compose.material.icons.rounded.ContentPaste
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Folder
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
|
@ -31,27 +32,32 @@ import androidx.compose.material3.OutlinedTextField
|
|||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import com.nuvio.app.core.ui.NuvioPrimaryButton
|
||||
import com.nuvio.app.core.ui.NuvioScreen
|
||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||
import com.nuvio.app.core.ui.NuvioSectionLabel
|
||||
import com.nuvio.app.core.ui.NuvioStatusModal
|
||||
import com.nuvio.app.core.ui.NuvioSurfaceCard
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -113,19 +119,14 @@ fun CollectionManagementScreen(
|
|||
item { NuvioSectionLabel(text = "YOUR COLLECTIONS") }
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = collections,
|
||||
key = { _, collection -> collection.id },
|
||||
) { index, collection ->
|
||||
CollectionListItem(
|
||||
collection = collection,
|
||||
index = index,
|
||||
totalCount = collections.size,
|
||||
onEdit = { onNavigateToEditor(collection.id) },
|
||||
onDelete = { showDeleteConfirm = collection.id },
|
||||
onMoveUp = { CollectionRepository.moveUp(index) },
|
||||
onMoveDown = { CollectionRepository.moveDown(index) },
|
||||
)
|
||||
if (collections.isNotEmpty()) {
|
||||
item {
|
||||
CollectionReorderableList(
|
||||
collections = collections,
|
||||
onEdit = { onNavigateToEditor(it) },
|
||||
onDelete = { showDeleteConfirm = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (collections.isEmpty()) {
|
||||
|
|
@ -201,16 +202,58 @@ fun CollectionManagementScreen(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CollectionReorderableList(
|
||||
collections: List<Collection>,
|
||||
onEdit: (String) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyListState = rememberReorderableLazyListState(
|
||||
lazyListState = lazyListState,
|
||||
) { from, to ->
|
||||
CollectionRepository.moveByIndex(from.index, to.index)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 720.dp),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
itemsIndexed(collections, key = { _, collection -> collection.id }) { _, collection ->
|
||||
ReorderableItem(reorderableLazyListState, key = collection.id) { isDragging ->
|
||||
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
shadowElevation = elevation,
|
||||
) {
|
||||
CollectionListItem(
|
||||
collection = collection,
|
||||
onEdit = { onEdit(collection.id) },
|
||||
onDelete = { onDelete(collection.id) },
|
||||
dragHandleScope = this@ReorderableItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CollectionListItem(
|
||||
collection: Collection,
|
||||
index: Int,
|
||||
totalCount: Int,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onMoveUp: () -> Unit,
|
||||
onMoveDown: () -> Unit,
|
||||
dragHandleScope: ReorderableCollectionItemScope,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
NuvioSurfaceCard {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -238,27 +281,26 @@ private fun CollectionListItem(
|
|||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onMoveUp,
|
||||
enabled = index > 0,
|
||||
modifier = Modifier.size(36.dp),
|
||||
modifier = with(dragHandleScope) {
|
||||
Modifier.draggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
},
|
||||
).size(36.dp)
|
||||
},
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ArrowUpward,
|
||||
contentDescription = "Move up",
|
||||
modifier = Modifier.size(20.dp).alpha(if (index > 0) 1f else 0.3f),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onMoveDown,
|
||||
enabled = index < totalCount - 1,
|
||||
modifier = Modifier.size(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ArrowDownward,
|
||||
contentDescription = "Move down",
|
||||
modifier = Modifier.size(20.dp).alpha(if (index < totalCount - 1) 1f else 0.3f),
|
||||
imageVector = Icons.Rounded.Menu,
|
||||
contentDescription = "Reorder",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
|
|
|||
|
|
@ -76,21 +76,20 @@ object CollectionRepository {
|
|||
}
|
||||
|
||||
fun moveUp(index: Int) {
|
||||
ensureLoaded()
|
||||
val list = _collections.value.toMutableList()
|
||||
if (index <= 0 || index >= list.size) return
|
||||
val item = list.removeAt(index)
|
||||
list.add(index - 1, item)
|
||||
_collections.value = list
|
||||
persist()
|
||||
moveByIndex(index, index - 1)
|
||||
}
|
||||
|
||||
fun moveDown(index: Int) {
|
||||
moveByIndex(index, index + 1)
|
||||
}
|
||||
|
||||
fun moveByIndex(fromIndex: Int, toIndex: Int) {
|
||||
ensureLoaded()
|
||||
val list = _collections.value.toMutableList()
|
||||
if (index < 0 || index >= list.size - 1) return
|
||||
val item = list.removeAt(index)
|
||||
list.add(index + 1, item)
|
||||
if (fromIndex == toIndex) return
|
||||
if (fromIndex !in list.indices || toIndex !in list.indices) return
|
||||
val item = list.removeAt(fromIndex)
|
||||
list.add(toIndex, item)
|
||||
_collections.value = list
|
||||
persist()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,22 +24,34 @@ import kotlinx.cinterop.addressOf
|
|||
import kotlinx.cinterop.reinterpret
|
||||
import platform.CoreGraphics.CGImageRef
|
||||
import platform.CoreFoundation.CFDataCreate
|
||||
import platform.ImageIO.CGImageSourceCopyPropertiesAtIndex
|
||||
import platform.ImageIO.CGImageSourceCreateImageAtIndex
|
||||
import platform.ImageIO.CGImageSourceCreateWithData
|
||||
import platform.ImageIO.CGImageSourceGetCount
|
||||
import platform.ImageIO.kCGImagePropertyGIFDelayTime
|
||||
import platform.ImageIO.kCGImagePropertyGIFDictionary
|
||||
import platform.ImageIO.kCGImagePropertyGIFUnclampedDelayTime
|
||||
import platform.UIKit.UIImage
|
||||
import platform.UIKit.UIImageView
|
||||
import platform.UIKit.UIViewContentMode
|
||||
import platform.CoreGraphics.CGImageRelease
|
||||
import kotlinx.cinterop.usePinned
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val gifHttpClient = HttpClient(Darwin)
|
||||
private val gifDecodeScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private const val MaxCachedGifImages = 12
|
||||
private const val DefaultGifFrameDurationSeconds = 0.1
|
||||
private const val MinimumGifFrameDurationSeconds = 0.02
|
||||
private val gifImageCache = mutableMapOf<String, UIImage>()
|
||||
private val gifImageCacheOrder = mutableListOf<String>()
|
||||
private val gifImageInFlight = mutableMapOf<String, Deferred<UIImage?>>()
|
||||
|
||||
private data class GifFrame(
|
||||
val image: UIImage,
|
||||
val durationSeconds: Double,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
@Composable
|
||||
internal actual fun CollectionCardRemoteImage(
|
||||
|
|
@ -147,12 +159,17 @@ private fun UIImage.Companion.gifImageWithData(data: kotlinx.cinterop.CPointer<c
|
|||
return runCatching {
|
||||
val source = data?.let { CGImageSourceCreateWithData(it, null) } ?: return null
|
||||
val count = CGImageSourceGetCount(source).toInt()
|
||||
val frames = mutableListOf<UIImage>()
|
||||
val frames = mutableListOf<GifFrame>()
|
||||
|
||||
for (index in 0 until count) {
|
||||
val imageRef: CGImageRef = CGImageSourceCreateImageAtIndex(source, index.toULong(), null) ?: continue
|
||||
try {
|
||||
frames.add(UIImage.imageWithCGImage(imageRef))
|
||||
frames.add(
|
||||
GifFrame(
|
||||
image = UIImage.imageWithCGImage(imageRef),
|
||||
durationSeconds = gifFrameDurationSeconds(source, index),
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
CGImageRelease(imageRef)
|
||||
}
|
||||
|
|
@ -160,7 +177,43 @@ private fun UIImage.Companion.gifImageWithData(data: kotlinx.cinterop.CPointer<c
|
|||
|
||||
if (frames.isEmpty()) return null
|
||||
|
||||
val durationSeconds = (count * 0.1).coerceAtLeast(0.1)
|
||||
UIImage.animatedImageWithImages(frames, durationSeconds)
|
||||
val animatedFrames = expandedGifFrames(frames)
|
||||
val durationSeconds = animatedFrames.size * MinimumGifFrameDurationSeconds
|
||||
UIImage.animatedImageWithImages(animatedFrames, durationSeconds)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun gifFrameDurationSeconds(source: Any, index: Int): Double {
|
||||
val properties = CGImageSourceCopyPropertiesAtIndex(source, index.toULong(), null) as? Map<Any?, *>
|
||||
?: return DefaultGifFrameDurationSeconds
|
||||
val gifProperties = properties[kCGImagePropertyGIFDictionary] as? Map<Any?, *>
|
||||
?: return DefaultGifFrameDurationSeconds
|
||||
|
||||
val unclampedDelay = gifProperties.doubleValue(kCGImagePropertyGIFUnclampedDelayTime)
|
||||
if (unclampedDelay != null && unclampedDelay >= MinimumGifFrameDurationSeconds) {
|
||||
return unclampedDelay
|
||||
}
|
||||
|
||||
val delay = gifProperties.doubleValue(kCGImagePropertyGIFDelayTime)
|
||||
return when {
|
||||
delay == null -> DefaultGifFrameDurationSeconds
|
||||
delay < MinimumGifFrameDurationSeconds -> MinimumGifFrameDurationSeconds
|
||||
else -> delay
|
||||
}
|
||||
}
|
||||
|
||||
private fun expandedGifFrames(frames: List<GifFrame>): List<UIImage> {
|
||||
val expandedFrames = ArrayList<UIImage>(frames.size)
|
||||
frames.forEach { frame ->
|
||||
val repeatCount = (frame.durationSeconds / MinimumGifFrameDurationSeconds)
|
||||
.roundToInt()
|
||||
.coerceAtLeast(1)
|
||||
repeat(repeatCount) {
|
||||
expandedFrames.add(frame.image)
|
||||
}
|
||||
}
|
||||
return expandedFrames
|
||||
}
|
||||
|
||||
private fun Map<Any?, *>.doubleValue(key: Any?): Double? =
|
||||
(this[key] as? Number)?.toDouble()
|
||||
Loading…
Reference in a new issue