diff --git a/native/app/src/main/java/com/localstream/app/domain/VideoFilterSorter.kt b/native/app/src/main/java/com/localstream/app/domain/VideoFilterSorter.kt index 2138603..9e52cdb 100644 --- a/native/app/src/main/java/com/localstream/app/domain/VideoFilterSorter.kt +++ b/native/app/src/main/java/com/localstream/app/domain/VideoFilterSorter.kt @@ -40,13 +40,51 @@ object VideoFilterSorter { } private fun sortVideos(videos: List, opts: FilterSortOptions): List { - return videos.sortedWith { a, b -> - val aWatched = isItemWatched(a, opts.watchedVideos) - val bWatched = isItemWatched(b, opts.watchedVideos) + val watchedCache = videos.associateWith { isItemWatched(it, opts.watchedVideos) } + val watchedComparator = Comparator { a, b -> + val aWatched = watchedCache[a] ?: false + val bWatched = watchedCache[b] ?: false if (aWatched != bWatched) { if (aWatched) 1 else -1 } else { - compareItemsByCriteria(a, b, opts) + 0 + } + } + val criteriaComparator = getCriteriaComparator(videos, opts) + return videos.sortedWith(watchedComparator.then(criteriaComparator)) + } + + private fun getCriteriaComparator( + videos: List, + opts: FilterSortOptions, + ): Comparator { + return when (opts.sortBy) { + SortBy.ALPHA -> { + val alphaCache = videos.associateWith { if (it.isSeriesGroup) it.seriesName.orEmpty() else it.name } + compareBy { alphaCache[it].orEmpty() } + } + SortBy.DATE -> { + val dateCache = videos.associateWith { + val lookup = if (it.isSeriesGroup) it.seriesName.orEmpty() else it.name + opts.releaseDates[lookup] ?: it.lastModified.toString() + } + compareByDescending { dateCache[it].orEmpty() } + } + SortBy.SIZE -> { + val sizeCache = videos.associateWith { + if (it.isSeriesGroup) it.episodes.orEmpty().sumOf { ep -> ep.size } else it.size + } + compareByDescending { sizeCache[it] ?: 0L } + } + SortBy.DURATION -> { + val durationCache = videos.associateWith { + if (it.isSeriesGroup) { + it.episodes.orEmpty().sumOf { ep -> opts.videoDurations[ep.name] ?: 0L } + } else { + opts.videoDurations[it.name] ?: 0L + } + } + compareByDescending { durationCache[it] ?: 0L } } } } @@ -58,40 +96,5 @@ object VideoFilterSorter { watchedVideos[v.name] == true } } - - private fun compareItemsByCriteria(a: VideoItem, b: VideoItem, opts: FilterSortOptions): Int { - return when (opts.sortBy) { - SortBy.ALPHA -> compareAlpha(a, b) - SortBy.DATE -> compareDate(a, b, opts.releaseDates) - SortBy.SIZE -> compareSize(a, b) - SortBy.DURATION -> compareDuration(a, b, opts.videoDurations) - } - } - - private fun compareAlpha(a: VideoItem, b: VideoItem): Int { - val nameA = if (a.isSeriesGroup) a.seriesName ?: "" else a.name - val nameB = if (b.isSeriesGroup) b.seriesName ?: "" else b.name - return nameA.compareTo(nameB) - } - - private fun compareDate(a: VideoItem, b: VideoItem, releaseDates: Map): Int { - val lookupA = if (a.isSeriesGroup) a.seriesName ?: "" else a.name - val lookupB = if (b.isSeriesGroup) b.seriesName ?: "" else b.name - val dateA = releaseDates[lookupA] ?: a.lastModified.toString() - val dateB = releaseDates[lookupB] ?: b.lastModified.toString() - return dateB.compareTo(dateA) - } - - private fun compareSize(a: VideoItem, b: VideoItem): Int { - val sizeA = if (a.isSeriesGroup) a.episodes.orEmpty().sumOf { it.size } else a.size - val sizeB = if (b.isSeriesGroup) b.episodes.orEmpty().sumOf { it.size } else b.size - return sizeB.compareTo(sizeA) - } - - private fun compareDuration(a: VideoItem, b: VideoItem, videoDurations: Map): Int { - val durA = if (a.isSeriesGroup) a.episodes.orEmpty().sumOf { videoDurations[it.name] ?: 0L } else videoDurations[a.name] ?: 0L - val durB = if (b.isSeriesGroup) b.episodes.orEmpty().sumOf { videoDurations[it.name] ?: 0L } else videoDurations[b.name] ?: 0L - return durB.compareTo(durA) - } } diff --git a/native/app/src/main/java/com/localstream/app/ui/history/HistoryViewModel.kt b/native/app/src/main/java/com/localstream/app/ui/history/HistoryViewModel.kt index a85b186..afc8132 100644 --- a/native/app/src/main/java/com/localstream/app/ui/history/HistoryViewModel.kt +++ b/native/app/src/main/java/com/localstream/app/ui/history/HistoryViewModel.kt @@ -42,7 +42,8 @@ class HistoryViewModel( container.settingsRepository.observeForceAvailable, ) { watchedSet: Set, playbackMap: Map, diskVideos: List, forceSet: Set -> val allNames = (watchedSet + playbackMap.keys).distinct() - val diskVideoNames = diskVideos.map { it.name }.toSet() + val diskVideoMap = diskVideos.associateBy { it.name } + val diskVideoNames = diskVideoMap.keys val historyItems = allNames.map { name -> val pb = playbackMap[name] @@ -51,7 +52,7 @@ class HistoryViewModel( val isForceAvailable = forceSet.contains(name) val effectiveAvailable = isDiskAvailable || isForceAvailable - val diskVideo = diskVideos.find { it.name == name } + val diskVideo = diskVideoMap[name] val durationMs = (diskVideo?.duration ?: 0L) * 1000L val positionMs = pb?.positionMs ?: 0L val progressPercent = if (durationMs > 0L) { diff --git a/native/app/src/main/java/com/localstream/app/ui/playlists/PlaylistsViewModel.kt b/native/app/src/main/java/com/localstream/app/ui/playlists/PlaylistsViewModel.kt index 641b74f..79c025d 100644 --- a/native/app/src/main/java/com/localstream/app/ui/playlists/PlaylistsViewModel.kt +++ b/native/app/src/main/java/com/localstream/app/ui/playlists/PlaylistsViewModel.kt @@ -33,8 +33,9 @@ class PlaylistsViewModel( container.videoRepository.observeVideos, ) { playlists, selectedId, videos -> val selected = playlists.find { it.id == selectedId } + val videoMap = videos.associateBy { it.name } val playlistVideos = selected?.videoNames?.mapNotNull { name -> - videos.find { it.name == name } + videoMap[name] } ?: emptyList() PlaylistsUiState( diff --git a/native/app/src/test/java/com/localstream/app/domain/SortingTest.kt b/native/app/src/test/java/com/localstream/app/domain/SortingTest.kt index 0cf0736..da5a678 100644 --- a/native/app/src/test/java/com/localstream/app/domain/SortingTest.kt +++ b/native/app/src/test/java/com/localstream/app/domain/SortingTest.kt @@ -46,5 +46,34 @@ class SortingTest { assertEquals(1, res.size) assertEquals("Film.1080p.mkv", res[0].name) } + + @Test + fun filterAndSortVideos_sortsByDateDescending() { + val opts = baseOpts.copy( + sortBy = SortBy.DATE, + releaseDates = mapOf("Old.mkv" to "2010-01-01", "New.mkv" to "2024-05-01"), + ) + val res = VideoFilterSorter.filterAndSortVideos(listOf(v("Old.mkv"), v("New.mkv")), opts) + assertEquals(listOf("New.mkv", "Old.mkv"), res.map { it.name }) + } + + @Test + fun filterAndSortVideos_sortsBySizeDescending() { + val opts = baseOpts.copy(sortBy = SortBy.SIZE) + val small = v("Small.mkv").copy(size = 100L) + val large = v("Large.mkv").copy(size = 5000L) + val res = VideoFilterSorter.filterAndSortVideos(listOf(small, large), opts) + assertEquals(listOf("Large.mkv", "Small.mkv"), res.map { it.name }) + } + + @Test + fun filterAndSortVideos_sortsByDurationDescending() { + val opts = baseOpts.copy( + sortBy = SortBy.DURATION, + videoDurations = mapOf("Short.mkv" to 60L, "Long.mkv" to 3600L), + ) + val res = VideoFilterSorter.filterAndSortVideos(listOf(v("Short.mkv"), v("Long.mkv")), opts) + assertEquals(listOf("Long.mkv", "Short.mkv"), res.map { it.name }) + } }