From e856303181d4319325168db8b9104c9c1ef3f259 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sun, 8 Dec 2024 10:44:52 -0700 Subject: [PATCH] Move to `IdentifiedArray` in `PagingLibraryViewModel` (#1346) * init * update packages, cleanup --- Shared/Strings/Strings.swift | 4 +- .../PagingLibraryViewModel.swift | 9 +- .../ViewModels/MediaViewModel/MediaType.swift | 16 +- Swiftfin tvOS/Components/PosterHStack.swift | 45 ++---- .../ChannelLibraryView.swift | 2 +- .../Components/LatestInLibraryView.swift | 2 +- .../HomeView/Components/NextUpView.swift | 2 +- .../Components/RecentlyAddedView.swift | 2 +- .../Components/EpisodeHStack.swift | 8 +- .../Components/SimilarItemsHStack.swift | 2 +- Swiftfin tvOS/Views/MediaView/MediaView.swift | 2 +- .../PagingLibraryView/PagingLibraryView.swift | 4 +- Swiftfin.xcodeproj/project.pbxproj | 147 ++++++++++++------ .../xcshareddata/swiftpm/Package.resolved | 15 +- Swiftfin/Components/PosterHStack.swift | 42 ++--- .../ActiveSessionsView.swift | 3 +- .../ChannelLibraryView.swift | 4 +- .../Components/ContinueWatchingView.swift | 2 +- .../Components/LatestInLibraryView.swift | 2 +- .../HomeView/Components/NextUpView.swift | 2 +- .../Components/RecentlyAddedView.swift | 2 +- .../Components/AboutView/AboutView.swift | 20 ++- .../Components/EpisodeHStack.swift | 10 +- .../Components/SimilarItemsHStack.swift | 2 +- Swiftfin/Views/MediaView/MediaView.swift | 2 +- .../PagingLibraryView/PagingLibraryView.swift | 8 +- .../VideoPlayer/Overlays/ChapterOverlay.swift | 7 +- 27 files changed, 215 insertions(+), 151 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 8957324b..bf847175 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1086,10 +1086,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Runtime - internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Run Time internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") + /// Runtime + internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift index e0a1e93c..7babfdcf 100644 --- a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift @@ -10,6 +10,7 @@ import Combine import Defaults import Foundation import Get +import IdentifiedCollections import JellyfinAPI import OrderedCollections import UIKit @@ -32,7 +33,7 @@ private let DefaultPageSize = 50 on remembering other filters. */ -class PagingLibraryViewModel: ViewModel, Eventful, Stateful { +class PagingLibraryViewModel: ViewModel, Eventful, Stateful { // MARK: Event @@ -67,7 +68,7 @@ class PagingLibraryViewModel: ViewModel, Eventful, Stateful { @Published final var backgroundStates: OrderedSet = [] @Published - final var elements: OrderedSet + final var elements: IdentifiedArrayOf @Published final var state: State = .initial @Published @@ -103,7 +104,7 @@ class PagingLibraryViewModel: ViewModel, Eventful, Stateful { parent: (any LibraryParent)? = nil ) { self.filterViewModel = nil - self.elements = OrderedSet(data) + self.elements = IdentifiedArray(uniqueElements: data) self.isStatic = true self.hasNextPage = false self.pageSize = DefaultPageSize @@ -130,7 +131,7 @@ class PagingLibraryViewModel: ViewModel, Eventful, Stateful { filters: ItemFilterCollection? = nil, pageSize: Int = DefaultPageSize ) { - self.elements = OrderedSet() + self.elements = IdentifiedArray() self.isStatic = false self.pageSize = pageSize self.parent = parent diff --git a/Shared/ViewModels/MediaViewModel/MediaType.swift b/Shared/ViewModels/MediaViewModel/MediaType.swift index 462229c0..b25bd02f 100644 --- a/Shared/ViewModels/MediaViewModel/MediaType.swift +++ b/Shared/ViewModels/MediaViewModel/MediaType.swift @@ -11,7 +11,8 @@ import JellyfinAPI extension MediaViewModel { - enum MediaType: Displayable, Hashable { + enum MediaType: Displayable, Hashable, Identifiable { + case collectionFolder(BaseItemDto) case downloads case favorites @@ -29,5 +30,18 @@ extension MediaViewModel { return L10n.liveTV } } + + var id: String? { + switch self { + case let .collectionFolder(item): + return item.id + case .downloads: + return "downloads" + case .favorites: + return "favorites" + case let .liveTV(item): + return item.id + } + } } } diff --git a/Swiftfin tvOS/Components/PosterHStack.swift b/Swiftfin tvOS/Components/PosterHStack.swift index b5c7e500..10af0e78 100644 --- a/Swiftfin tvOS/Components/PosterHStack.swift +++ b/Swiftfin tvOS/Components/PosterHStack.swift @@ -12,19 +12,19 @@ import SwiftUI // TODO: trailing content refactor? -struct PosterHStack: View { +struct PosterHStack: View where Data.Element == Element, Data.Index == Int { + private var data: Data private var title: String? private var type: PosterDisplayType - private var items: Binding> - private var content: (Item) -> any View - private var imageOverlay: (Item) -> any View - private var contextMenu: (Item) -> any View + private var content: (Element) -> any View + private var imageOverlay: (Element) -> any View + private var contextMenu: (Element) -> any View private var trailingContent: () -> any View - private var onSelect: (Item) -> Void + private var onSelect: (Element) -> Void // See PosterButton for implementation reason - private var focusedItem: Binding? + private var focusedItem: Binding? var body: some View { VStack(alignment: .leading, spacing: 20) { @@ -42,7 +42,7 @@ struct PosterHStack: View { } CollectionHStack( - items, + uniqueElements: data, columns: type == .landscape ? 4 : 7 ) { item in PosterButton(item: item, type: type) @@ -86,42 +86,29 @@ extension PosterHStack { init( title: String? = nil, type: PosterDisplayType, - items: Binding> + items: Data ) { self.init( + data: items, title: title, type: type, - items: items, content: { PosterButton.TitleSubtitleContentView(item: $0) }, imageOverlay: { PosterButton.DefaultOverlay(item: $0) }, contextMenu: { _ in EmptyView() }, trailingContent: { EmptyView() }, - onSelect: { _ in }, - focusedItem: nil + onSelect: { _ in } ) } - init>( - title: String? = nil, - type: PosterDisplayType, - items: S - ) { - self.init( - title: title, - type: type, - items: .constant(OrderedSet(items)) - ) - } - - func content(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { + func content(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self { copy(modifying: \.content, with: content) } - func imageOverlay(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { + func imageOverlay(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self { copy(modifying: \.imageOverlay, with: content) } - func contextMenu(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { + func contextMenu(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self { copy(modifying: \.contextMenu, with: content) } @@ -129,11 +116,11 @@ extension PosterHStack { copy(modifying: \.trailingContent, with: content) } - func onSelect(_ action: @escaping (Item) -> Void) -> Self { + func onSelect(_ action: @escaping (Element) -> Void) -> Self { copy(modifying: \.onSelect, with: action) } - func focusedItem(_ binding: Binding) -> Self { + func focusedItem(_ binding: Binding) -> Self { copy(modifying: \.focusedItem, with: binding) } } diff --git a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift index f91db188..f8cae264 100644 --- a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift +++ b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift @@ -22,7 +22,7 @@ struct ChannelLibraryView: View { @ViewBuilder private var contentView: some View { CollectionVGrid( - $viewModel.elements, + uniqueElements: viewModel.elements, layout: .columns(3, insets: .init(0), itemSpacing: 25, lineSpacing: 25) ) { channel in WideChannelGridItem(channel: channel) diff --git a/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift index 9ab31ce7..84b2cd16 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift @@ -24,7 +24,7 @@ extension HomeView { PosterHStack( title: L10n.latestWithString(viewModel.parent?.displayTitle ?? .emptyDash), type: .portrait, - items: $viewModel.elements + items: viewModel.elements ) .onSelect { item in router.route(to: \.item, item) diff --git a/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift index b2e0b260..5e14053b 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift @@ -27,7 +27,7 @@ extension HomeView { PosterHStack( title: L10n.nextUp, type: nextUpPosterType, - items: $viewModel.elements + items: viewModel.elements ) .onSelect { item in router.route(to: \.item, item) diff --git a/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift index 41c5bdeb..3d627b33 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift @@ -27,7 +27,7 @@ extension HomeView { PosterHStack( title: L10n.recentlyAdded, type: recentlyAddedPosterType, - items: $viewModel.elements + items: viewModel.elements ) .onSelect { item in router.route(to: \.item, item) diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index 36937358..b672c604 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -30,13 +30,13 @@ extension SeriesEpisodeSelector { private var lastFocusedEpisodeID: String? @StateObject - private var proxy = CollectionHStackProxy() + private var proxy = CollectionHStackProxy() let playButtonItem: BaseItemDto? private func contentView(viewModel: SeasonItemViewModel) -> some View { CollectionHStack( - $viewModel.elements, + uniqueElements: viewModel.elements, columns: 3.5 ) { episode in SeriesEpisodeSelector.EpisodeCard(episode: episode) @@ -103,7 +103,7 @@ extension SeriesEpisodeSelector { var body: some View { CollectionHStack( - 0 ..< 1, + count: 1, columns: 3.5 ) { _ in SeriesEpisodeSelector.ErrorCard(error: error) @@ -121,7 +121,7 @@ extension SeriesEpisodeSelector { var body: some View { CollectionHStack( - 0 ..< Int.random(in: 2 ..< 5), + count: Int.random(in: 2 ..< 5), columns: 3.5 ) { _ in SeriesEpisodeSelector.LoadingCard() diff --git a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift index 1f712ba8..992985e5 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift @@ -31,7 +31,7 @@ extension ItemView { PosterHStack( title: L10n.recommended, type: similarPosterType, - items: $viewModel.elements + items: viewModel.elements ) .onSelect { item in router.route(to: \.item, item) diff --git a/Swiftfin tvOS/Views/MediaView/MediaView.swift b/Swiftfin tvOS/Views/MediaView/MediaView.swift index 25743ca4..6142869a 100644 --- a/Swiftfin tvOS/Views/MediaView/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView/MediaView.swift @@ -23,7 +23,7 @@ struct MediaView: View { @ViewBuilder private var contentView: some View { CollectionVGrid( - $viewModel.mediaItems, + uniqueElements: viewModel.mediaItems, layout: .columns(4, insets: .init(50), itemSpacing: 50, lineSpacing: 50) ) { mediaType in MediaItem(viewModel: viewModel, type: mediaType) diff --git a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift index 19f4f4c2..d1a8d153 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift @@ -16,7 +16,7 @@ import SwiftUI // TODO: list row view (LibraryRow) // TODO: fix paging for next item focusing the tab -struct PagingLibraryView: View { +struct PagingLibraryView: View { @Default(.Customization.Library.cinematicBackground) private var cinematicBackground @@ -159,7 +159,7 @@ struct PagingLibraryView: View { @ViewBuilder private var contentView: some View { CollectionVGrid( - $viewModel.elements, + uniqueElements: viewModel.elements, layout: layout ) { item in switch (posterType, viewType) { diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index da6e05af..3549bd95 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -453,13 +453,12 @@ E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153D9B2BBA3E9D00424D36 /* LoadingCard.swift */; }; E1153DA42BBA614F00424D36 /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DA32BBA614F00424D36 /* CollectionVGrid */; }; E1153DAC2BBA6AD200424D36 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DAB2BBA6AD200424D36 /* CollectionHStack */; }; - E1153DAF2BBA734200424D36 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DAE2BBA734200424D36 /* CollectionHStack */; }; - E1153DB12BBA734C00424D36 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DB02BBA734C00424D36 /* CollectionHStack */; }; E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153DB22BBA80B400424D36 /* EmptyCard.swift */; }; E1153DCC2BBB633B00424D36 /* FastSVGView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153DCB2BBB633B00424D36 /* FastSVGView.swift */; }; E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153DCB2BBB633B00424D36 /* FastSVGView.swift */; }; E1153DD02BBB634F00424D36 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DCF2BBB634F00424D36 /* SVGKit */; }; E1153DD22BBB649C00424D36 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DD12BBB649C00424D36 /* SVGKit */; }; + E1155ACB2D0584A90021557A /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E1155ACA2D0584A90021557A /* IdentifiedCollections */; }; E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11562942C818CB2001D5DE4 /* BindingBox.swift */; }; E11562962C818CB2001D5DE4 /* BindingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11562942C818CB2001D5DE4 /* BindingBox.swift */; }; E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; }; @@ -525,8 +524,6 @@ E12E30F5296392EC0022FAC9 /* EnumPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */; }; E12F038C28F8B0B100976CC3 /* EdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */; }; E132D3C82BD200C10058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3C72BD200C10058A2DF /* CollectionVGrid */; }; - E132D3CD2BD2179C0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CC2BD2179C0058A2DF /* CollectionVGrid */; }; - E132D3CF2BD217AA0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CE2BD217AA0058A2DF /* CollectionVGrid */; }; E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */; }; E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */; }; E133328829538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; }; @@ -749,6 +746,10 @@ E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; }; E1763A742BF3FA4C004DF6AB /* AppLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */; }; E1763A762BF3FF01004DF6AB /* AppLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A752BF3FF01004DF6AB /* AppLoadingView.swift */; }; + E176EBDE2D050067009F4CF1 /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E176EBDD2D050067009F4CF1 /* IdentifiedCollections */; }; + E176EBE02D0502A6009F4CF1 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E176EBDF2D0502A6009F4CF1 /* CollectionHStack */; }; + E176EBE32D0502C6009F4CF1 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E176EBE22D0502C6009F4CF1 /* CollectionHStack */; }; + E176EBE92D050925009F4CF1 /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E176EBE82D050925009F4CF1 /* CollectionVGrid */; }; E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859A2780F1F40094FBCF /* tvOSSlider.swift */; }; E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; }; E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.swift */; }; @@ -878,6 +879,10 @@ E19E6E0728A0B958005C10C8 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E19E6E0628A0B958005C10C8 /* NukeUI */; }; E19E6E0A28A0BEFF005C10C8 /* BlurHashKit in Frameworks */ = {isa = PBXBuildFile; productRef = E19E6E0928A0BEFF005C10C8 /* BlurHashKit */; }; E19F6C5D28F5189300C5197E /* MediaStreamInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19F6C5C28F5189300C5197E /* MediaStreamInfoView.swift */; }; + E1A09F722D05933D00835265 /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E1A09F712D05933D00835265 /* CollectionVGrid */; }; + E1A09F752D05935100835265 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1A09F742D05935100835265 /* CollectionHStack */; }; + E1A09F772D05935A00835265 /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E1A09F762D05935A00835265 /* CollectionVGrid */; }; + E1A09F792D05935A00835265 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1A09F782D05935A00835265 /* CollectionHStack */; }; E1A1528228FD126C00600579 /* VerticalAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528128FD126C00600579 /* VerticalAlignment.swift */; }; E1A1528528FD191A00600579 /* TextPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528428FD191A00600579 /* TextPair.swift */; }; E1A1528828FD229500600579 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528728FD229500600579 /* ChevronButton.swift */; }; @@ -1934,6 +1939,7 @@ E13AF3B828A0C598009093AB /* NukeExtensions in Frameworks */, E1575E58293E7685001665B1 /* Files in Frameworks */, E1B5F7A729577BCE004B26CF /* Pulse in Frameworks */, + E1A09F792D05935A00835265 /* CollectionHStack in Frameworks */, E13AF3BA28A0C598009093AB /* NukeUI in Frameworks */, E1B5F7AB29577BCE004B26CF /* PulseUI in Frameworks */, E1B5F7A929577BCE004B26CF /* PulseLogHandler in Frameworks */, @@ -1943,18 +1949,18 @@ 62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */, E19D41B22BF2BFA50082B8B2 /* KeychainSwift in Frameworks */, E18443CB2A037773002DDDC8 /* UDPBroadcast in Frameworks */, + E1155ACB2D0584A90021557A /* IdentifiedCollections in Frameworks */, 62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */, E1B5F7AD29577BDD004B26CF /* OrderedCollections in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, 62666E1F27E501DF00EC0ECD /* CoreText.framework in Frameworks */, E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */, E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */, + E1A09F772D05935A00835265 /* CollectionVGrid in Frameworks */, E1153DD22BBB649C00424D36 /* SVGKit in Frameworks */, 62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */, - E132D3CF2BD217AA0058A2DF /* CollectionVGrid in Frameworks */, E13AF3BC28A0C59E009093AB /* BlurHashKit in Frameworks */, E150C0C32BFD6DA200944FFA /* JellyfinAPI in Frameworks */, - E1153DB12BBA734C00424D36 /* CollectionHStack in Frameworks */, 62666E1327E501C300EC0ECD /* AudioToolbox.framework in Frameworks */, E13AF3B628A0C598009093AB /* Nuke in Frameworks */, E12186DE2718F1C50010884C /* Defaults in Frameworks */, @@ -1981,6 +1987,7 @@ 62666E0627E5017A00EC0ECD /* CoreVideo.framework in Frameworks */, E19DDEC72948EF9900954E10 /* OrderedCollections in Frameworks */, E10706102942F57D00646DAF /* Pulse in Frameworks */, + E176EBE92D050925009F4CF1 /* CollectionVGrid in Frameworks */, E192608328D2D0DB002314B4 /* Factory in Frameworks */, E150C0C12BFD62FD00944FFA /* JellyfinAPI in Frameworks */, E113A2A72B5A178D009CAAAA /* CollectionHStack in Frameworks */, @@ -1990,7 +1997,9 @@ 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */, 62666E0227E5016D00EC0ECD /* CoreGraphics.framework in Frameworks */, E1575E3C293C6B15001665B1 /* Files in Frameworks */, + E176EBE02D0502A6009F4CF1 /* CollectionHStack in Frameworks */, E14EA1652BF70A8E00DE757A /* Mantis in Frameworks */, + E176EBDE2D050067009F4CF1 /* IdentifiedCollections in Frameworks */, 62666E1027E501B400EC0ECD /* VideoToolbox.framework in Frameworks */, 62666E0C27E501A500EC0ECD /* OpenGLES.framework in Frameworks */, E19E6E0A28A0BEFF005C10C8 /* BlurHashKit in Frameworks */, @@ -2003,22 +2012,23 @@ E18A8E7A28D5FEDF00333B9A /* VLCUI in Frameworks */, E114DB332B1944FA00B75FB3 /* CollectionVGrid in Frameworks */, E15210562946DF1B00375CC2 /* PulseLogHandler in Frameworks */, - E1153DAF2BBA734200424D36 /* CollectionHStack in Frameworks */, 62666E0427E5017500EC0ECD /* CoreText.framework in Frameworks */, - E132D3CD2BD2179C0058A2DF /* CollectionVGrid in Frameworks */, E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */, + E176EBE32D0502C6009F4CF1 /* CollectionHStack in Frameworks */, 62666E0E27E501AF00EC0ECD /* Security.framework in Frameworks */, E1DC9814296DC06200982F06 /* PulseLogHandler in Frameworks */, E15EFA842BA167350080E926 /* CollectionHStack in Frameworks */, E15EFA862BA1685F0080E926 /* SwiftUIIntrospect in Frameworks */, 62666DFE27E5015700EC0ECD /* AVFoundation.framework in Frameworks */, 62666DFD27E5014F00EC0ECD /* AudioToolbox.framework in Frameworks */, + E1A09F722D05933D00835265 /* CollectionVGrid in Frameworks */, E19E6E0528A0B958005C10C8 /* Nuke in Frameworks */, E1153DAC2BBA6AD200424D36 /* CollectionHStack in Frameworks */, 62666E0D27E501AA00EC0ECD /* QuartzCore.framework in Frameworks */, E15D4F052B1B0C3C00442DB8 /* PreferencesView in Frameworks */, E19E6E0728A0B958005C10C8 /* NukeUI in Frameworks */, 62666E3F27E5040300EC0ECD /* SystemConfiguration.framework in Frameworks */, + E1A09F752D05935100835265 /* CollectionHStack in Frameworks */, 62666E3927E502CE00EC0ECD /* SwizzleSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4611,11 +4621,12 @@ E18443CA2A037773002DDDC8 /* UDPBroadcast */, E1A7B1642B9A9F7800152546 /* PreferencesView */, E1392FEC2BA218A80034110D /* SwiftUIIntrospect */, - E1153DB02BBA734C00424D36 /* CollectionHStack */, E1153DD12BBB649C00424D36 /* SVGKit */, - E132D3CE2BD217AA0058A2DF /* CollectionVGrid */, E19D41B12BF2BFA50082B8B2 /* KeychainSwift */, E150C0C22BFD6DA200944FFA /* JellyfinAPI */, + E1155ACA2D0584A90021557A /* IdentifiedCollections */, + E1A09F762D05935A00835265 /* CollectionVGrid */, + E1A09F782D05935A00835265 /* CollectionHStack */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */; @@ -4665,13 +4676,17 @@ E18D6AA52BAA96F000A0D167 /* CollectionHStack */, E1153DA32BBA614F00424D36 /* CollectionVGrid */, E1153DAB2BBA6AD200424D36 /* CollectionHStack */, - E1153DAE2BBA734200424D36 /* CollectionHStack */, E1153DCF2BBB634F00424D36 /* SVGKit */, E132D3C72BD200C10058A2DF /* CollectionVGrid */, - E132D3CC2BD2179C0058A2DF /* CollectionVGrid */, E145EB4A2BE16849003BF6F3 /* KeychainSwift */, E14EA1642BF70A8E00DE757A /* Mantis */, E150C0C02BFD62FD00944FFA /* JellyfinAPI */, + E176EBDD2D050067009F4CF1 /* IdentifiedCollections */, + E176EBDF2D0502A6009F4CF1 /* CollectionHStack */, + E176EBE22D0502C6009F4CF1 /* CollectionHStack */, + E176EBE82D050925009F4CF1 /* CollectionVGrid */, + E1A09F712D05933D00835265 /* CollectionVGrid */, + E1A09F742D05935100835265 /* CollectionHStack */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; @@ -4739,12 +4754,13 @@ E1DC9812296DC06200982F06 /* XCRemoteSwiftPackageReference "PulseLogHandler" */, E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */, E15D4F032B1B0C3C00442DB8 /* XCLocalSwiftPackageReference "PreferencesView" */, - E1153DAD2BBA734200424D36 /* XCRemoteSwiftPackageReference "CollectionHStack" */, E1153DCE2BBB634F00424D36 /* XCRemoteSwiftPackageReference "SVGKit" */, - E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */, E145EB492BE16849003BF6F3 /* XCRemoteSwiftPackageReference "keychain-swift" */, E14EA1632BF70A8E00DE757A /* XCRemoteSwiftPackageReference "Mantis" */, E150C0BF2BFD62FD00944FFA /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, + E176EBDC2D050067009F4CF1 /* XCRemoteSwiftPackageReference "swift-identified-collections" */, + E1A09F702D05933D00835265 /* XCRemoteSwiftPackageReference "CollectionVGrid" */, + E1A09F732D05935100835265 /* XCRemoteSwiftPackageReference "CollectionHStack" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -6359,14 +6375,6 @@ minimumVersion = 2.0.0; }; }; - E1153DAD2BBA734200424D36 /* XCRemoteSwiftPackageReference "CollectionHStack" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/LePips/CollectionHStack"; - requirement = { - branch = main; - kind = branch; - }; - }; E1153DCE2BBB634F00424D36 /* XCRemoteSwiftPackageReference "SVGKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SVGKit/SVGKit"; @@ -6375,14 +6383,6 @@ minimumVersion = 3.0.0; }; }; - E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/LePips/CollectionVGrid"; - requirement = { - branch = main; - kind = branch; - }; - }; E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/JohnEstropia/CoreStore.git"; @@ -6439,6 +6439,14 @@ minimumVersion = 4.0.0; }; }; + E176EBDC2D050067009F4CF1 /* XCRemoteSwiftPackageReference "swift-identified-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-identified-collections"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; + }; + }; E18A8E7828D5FEDF00333B9A /* XCRemoteSwiftPackageReference "VLCUI" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/LePips/VLCUI"; @@ -6479,6 +6487,22 @@ minimumVersion = 1.0.0; }; }; + E1A09F702D05933D00835265 /* XCRemoteSwiftPackageReference "CollectionVGrid" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LePips/CollectionVGrid"; + requirement = { + branch = main; + kind = branch; + }; + }; + E1A09F732D05935100835265 /* XCRemoteSwiftPackageReference "CollectionHStack" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LePips/CollectionHStack"; + requirement = { + branch = main; + kind = branch; + }; + }; E1DC9812296DC06200982F06 /* XCRemoteSwiftPackageReference "PulseLogHandler" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/PulseLogHandler"; @@ -6558,16 +6582,6 @@ isa = XCSwiftPackageProductDependency; productName = CollectionHStack; }; - E1153DAE2BBA734200424D36 /* CollectionHStack */ = { - isa = XCSwiftPackageProductDependency; - package = E1153DAD2BBA734200424D36 /* XCRemoteSwiftPackageReference "CollectionHStack" */; - productName = CollectionHStack; - }; - E1153DB02BBA734C00424D36 /* CollectionHStack */ = { - isa = XCSwiftPackageProductDependency; - package = E1153DAD2BBA734200424D36 /* XCRemoteSwiftPackageReference "CollectionHStack" */; - productName = CollectionHStack; - }; E1153DCF2BBB634F00424D36 /* SVGKit */ = { isa = XCSwiftPackageProductDependency; package = E1153DCE2BBB634F00424D36 /* XCRemoteSwiftPackageReference "SVGKit" */; @@ -6578,6 +6592,11 @@ package = E1153DCE2BBB634F00424D36 /* XCRemoteSwiftPackageReference "SVGKit" */; productName = SVGKit; }; + E1155ACA2D0584A90021557A /* IdentifiedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = E176EBDC2D050067009F4CF1 /* XCRemoteSwiftPackageReference "swift-identified-collections" */; + productName = IdentifiedCollections; + }; E12186DD2718F1C50010884C /* Defaults */ = { isa = XCSwiftPackageProductDependency; package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; @@ -6587,16 +6606,6 @@ isa = XCSwiftPackageProductDependency; productName = CollectionVGrid; }; - E132D3CC2BD2179C0058A2DF /* CollectionVGrid */ = { - isa = XCSwiftPackageProductDependency; - package = E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */; - productName = CollectionVGrid; - }; - E132D3CE2BD217AA0058A2DF /* CollectionVGrid */ = { - isa = XCSwiftPackageProductDependency; - package = E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */; - productName = CollectionVGrid; - }; E1388A45293F0ABA009721B1 /* SwizzleSwift */ = { isa = XCSwiftPackageProductDependency; package = 62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */; @@ -6709,6 +6718,24 @@ package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = SwiftUIIntrospect; }; + E176EBDD2D050067009F4CF1 /* IdentifiedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = E176EBDC2D050067009F4CF1 /* XCRemoteSwiftPackageReference "swift-identified-collections" */; + productName = IdentifiedCollections; + }; + E176EBDF2D0502A6009F4CF1 /* CollectionHStack */ = { + isa = XCSwiftPackageProductDependency; + package = E176EBDC2D050067009F4CF1 /* XCRemoteSwiftPackageReference "swift-identified-collections" */; + productName = CollectionHStack; + }; + E176EBE22D0502C6009F4CF1 /* CollectionHStack */ = { + isa = XCSwiftPackageProductDependency; + productName = CollectionHStack; + }; + E176EBE82D050925009F4CF1 /* CollectionVGrid */ = { + isa = XCSwiftPackageProductDependency; + productName = CollectionVGrid; + }; E18443CA2A037773002DDDC8 /* UDPBroadcast */ = { isa = XCSwiftPackageProductDependency; package = E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */; @@ -6758,6 +6785,26 @@ package = E19E6E0828A0BEFF005C10C8 /* XCRemoteSwiftPackageReference "BlurHashKit" */; productName = BlurHashKit; }; + E1A09F712D05933D00835265 /* CollectionVGrid */ = { + isa = XCSwiftPackageProductDependency; + package = E1A09F702D05933D00835265 /* XCRemoteSwiftPackageReference "CollectionVGrid" */; + productName = CollectionVGrid; + }; + E1A09F742D05935100835265 /* CollectionHStack */ = { + isa = XCSwiftPackageProductDependency; + package = E1A09F732D05935100835265 /* XCRemoteSwiftPackageReference "CollectionHStack" */; + productName = CollectionHStack; + }; + E1A09F762D05935A00835265 /* CollectionVGrid */ = { + isa = XCSwiftPackageProductDependency; + package = E1A09F702D05933D00835265 /* XCRemoteSwiftPackageReference "CollectionVGrid" */; + productName = CollectionVGrid; + }; + E1A09F782D05935A00835265 /* CollectionHStack */ = { + isa = XCSwiftPackageProductDependency; + package = E1A09F732D05935100835265 /* XCRemoteSwiftPackageReference "CollectionHStack" */; + productName = CollectionHStack; + }; E1A7B1642B9A9F7800152546 /* PreferencesView */ = { isa = XCSwiftPackageProductDependency; package = E15D4F032B1B0C3C00442DB8 /* XCLocalSwiftPackageReference "PreferencesView" */; diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 38f911da..dd783e02 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "651194fc1966b57201a0de2cba27dc40798bbdf515febdc83f00d634d916fea4", + "originHash" : "b7189175c8066640649da818750e83deee8ef2f766db25c34025f23d451b301d", "pins" : [ { "identity" : "blurhashkit", @@ -25,7 +25,7 @@ "location" : "https://github.com/LePips/CollectionHStack", "state" : { "branch" : "main", - "revision" : "894b595185bbfce007d60b219ee3e4013884131c" + "revision" : "00beb78cc570ee1014a92eb1cd7a60c099bce5ec" } }, { @@ -34,7 +34,7 @@ "location" : "https://github.com/LePips/CollectionVGrid", "state" : { "branch" : "main", - "revision" : "91ba930a502761924204ae74a59ded05f3b7ef89" + "revision" : "4b1591321339481756af6157c9205051d7fe3040" } }, { @@ -181,6 +181,15 @@ "version" : "1.0.5" } }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", + "version" : "1.1.0" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", diff --git a/Swiftfin/Components/PosterHStack.swift b/Swiftfin/Components/PosterHStack.swift index 5b2a76e0..b8cd9d9d 100644 --- a/Swiftfin/Components/PosterHStack.swift +++ b/Swiftfin/Components/PosterHStack.swift @@ -10,23 +10,23 @@ import CollectionHStack import OrderedCollections import SwiftUI -struct PosterHStack: View { +struct PosterHStack: View where Data.Element == Element, Data.Index == Int { + private var data: Data private var header: () -> any View private var title: String? private var type: PosterDisplayType - private var items: Binding> - private var content: (Item) -> any View - private var imageOverlay: (Item) -> any View - private var contextMenu: (Item) -> any View + private var content: (Element) -> any View + private var imageOverlay: (Element) -> any View + private var contextMenu: (Element) -> any View private var trailingContent: () -> any View - private var onSelect: (Item) -> Void + private var onSelect: (Element) -> Void @ViewBuilder private var padHStack: some View { CollectionHStack( - items, - minWidth: type == .portrait ? 140 : 220 + uniqueElements: data, + columns: type == .portrait ? 140 : 220 ) { item in PosterButton( item: item, @@ -47,7 +47,7 @@ struct PosterHStack: View { @ViewBuilder private var phoneHStack: some View { CollectionHStack( - items, + uniqueElements: data, columns: type == .portrait ? 3 : 2 ) { item in PosterButton( @@ -94,13 +94,13 @@ extension PosterHStack { init( title: String? = nil, type: PosterDisplayType, - items: Binding> + items: Data ) { self.init( + data: items, header: { DefaultHeader(title: title) }, title: title, type: type, - items: items, content: { PosterButton.TitleSubtitleContentView(item: $0) }, imageOverlay: { PosterButton.DefaultOverlay(item: $0) }, contextMenu: { _ in EmptyView() }, @@ -109,31 +109,19 @@ extension PosterHStack { ) } - init>( - title: String? = nil, - type: PosterDisplayType, - items: S - ) { - self.init( - title: title, - type: type, - items: .constant(OrderedSet(items)) - ) - } - func header(@ViewBuilder _ header: @escaping () -> any View) -> Self { copy(modifying: \.header, with: header) } - func content(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { + func content(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self { copy(modifying: \.content, with: content) } - func imageOverlay(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { + func imageOverlay(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self { copy(modifying: \.imageOverlay, with: content) } - func contextMenu(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { + func contextMenu(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self { copy(modifying: \.contextMenu, with: content) } @@ -141,7 +129,7 @@ extension PosterHStack { copy(modifying: \.trailingContent, with: content) } - func onSelect(_ action: @escaping (Item) -> Void) -> Self { + func onSelect(_ action: @escaping (Element) -> Void) -> Self { copy(modifying: \.onSelect, with: action) } } diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift index 6d390cbb..90735f3e 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift @@ -32,7 +32,8 @@ struct ActiveSessionsView: View { L10n.noResults.text } else { CollectionVGrid( - viewModel.sessions.keys, + uniqueElements: viewModel.sessions.keys, + id: \.self, layout: .columns(1, insets: .zero, itemSpacing: 0, lineSpacing: 0) ) { id in ActiveSessionRow(box: viewModel.sessions[id]!) { diff --git a/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift b/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift index 3ff48842..23a58bcb 100644 --- a/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift +++ b/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift @@ -98,8 +98,8 @@ struct ChannelLibraryView: View { @ViewBuilder private var contentView: some View { CollectionVGrid( - $viewModel.elements, - layout: $layout + uniqueElements: viewModel.elements, + layout: layout ) { channel in switch channelDisplayType { case .grid: diff --git a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift index c18865e2..6b3495b4 100644 --- a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift +++ b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift @@ -34,7 +34,7 @@ extension HomeView { var body: some View { CollectionHStack( - $viewModel.resumeItems, + uniqueElements: viewModel.resumeItems, columns: columnCount ) { item in PosterButton(item: item, type: .landscape) diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift index 02c82c6a..a3aa83ae 100644 --- a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -30,7 +30,7 @@ extension HomeView { PosterHStack( title: L10n.latestWithString(viewModel.parent?.displayTitle ?? .emptyDash), type: latestInLibraryPosterType, - items: $viewModel.elements + items: viewModel.elements ) .trailing { SeeAllButton() diff --git a/Swiftfin/Views/HomeView/Components/NextUpView.swift b/Swiftfin/Views/HomeView/Components/NextUpView.swift index 98f3bd19..1e33535f 100644 --- a/Swiftfin/Views/HomeView/Components/NextUpView.swift +++ b/Swiftfin/Views/HomeView/Components/NextUpView.swift @@ -31,7 +31,7 @@ extension HomeView { PosterHStack( title: L10n.nextUp, type: nextUpPosterType, - items: $viewModel.elements + items: viewModel.elements ) .content { item in if item.type == .episode { diff --git a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift index 3a2d96f2..e8f3d058 100644 --- a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift +++ b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift @@ -28,7 +28,7 @@ extension HomeView { PosterHStack( title: L10n.recentlyAdded, type: recentlyAddedPosterType, - items: $viewModel.elements + items: viewModel.elements ) .trailing { SeeAllButton() diff --git a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift index f7c1f13e..f3bfcb06 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift @@ -22,12 +22,25 @@ extension ItemView { struct AboutView: View { - private enum AboutViewItem: Hashable { + private enum AboutViewItem: Hashable, Identifiable { case image case overview case mediaSource(MediaSourceInfo) case ratings + + var id: String? { + switch self { + case .image: + return "image" + case .overview: + return "overview" + case let .mediaSource(source): + return source.id + case .ratings: + return "ratings" + } + } } @Default(.accentColor) @@ -121,7 +134,10 @@ extension ItemView { .accessibility(addTraits: [.isHeader]) .edgePadding(.horizontal) - CollectionHStack($items, variadicWidths: true) { item in + CollectionHStack( + uniqueElements: items, + variadicWidths: true + ) { item in switch item { case .image: imageView diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index fd6f9873..f0a92bf1 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -24,13 +24,13 @@ extension SeriesEpisodeSelector { private var didScrollToPlayButtonItem = false @StateObject - private var proxy = CollectionHStackProxy() + private var proxy = CollectionHStackProxy() let playButtonItem: BaseItemDto? private func contentView(viewModel: SeasonItemViewModel) -> some View { CollectionHStack( - $viewModel.elements, + uniqueElements: viewModel.elements, columns: UIDevice.isPhone ? 1.5 : 3.5 ) { episode in SeriesEpisodeSelector.EpisodeCard(episode: episode) @@ -71,7 +71,7 @@ extension SeriesEpisodeSelector { var body: some View { CollectionHStack( - 0 ..< 1, + count: 1, columns: UIDevice.isPhone ? 1.5 : 3.5 ) { _ in SeriesEpisodeSelector.EmptyCard() @@ -92,7 +92,7 @@ extension SeriesEpisodeSelector { var body: some View { CollectionHStack( - 0 ..< 1, + count: 1, columns: UIDevice.isPhone ? 1.5 : 3.5 ) { _ in SeriesEpisodeSelector.ErrorCard(error: error) @@ -110,7 +110,7 @@ extension SeriesEpisodeSelector { var body: some View { CollectionHStack( - 0 ..< Int.random(in: 2 ..< 5), + count: Int.random(in: 2 ..< 5), columns: UIDevice.isPhone ? 1.5 : 3.5 ) { _ in SeriesEpisodeSelector.LoadingCard() diff --git a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift index 79776602..3e99f550 100644 --- a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift @@ -32,7 +32,7 @@ extension ItemView { PosterHStack( title: L10n.recommended, type: similarPosterType, - items: $viewModel.elements + items: viewModel.elements ) .trailing { SeeAllButton() diff --git a/Swiftfin/Views/MediaView/MediaView.swift b/Swiftfin/Views/MediaView/MediaView.swift index 7b0a7a01..929a910c 100644 --- a/Swiftfin/Views/MediaView/MediaView.swift +++ b/Swiftfin/Views/MediaView/MediaView.swift @@ -36,7 +36,7 @@ struct MediaView: View { @ViewBuilder private var contentView: some View { CollectionVGrid( - $viewModel.mediaItems, + uniqueElements: viewModel.mediaItems, layout: UIDevice.isPhone ? phoneLayout : padLayout ) { mediaType in MediaItem(viewModel: viewModel, type: mediaType) diff --git a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift index b259688e..731f318d 100644 --- a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift @@ -36,7 +36,7 @@ import SwiftUI should be applied. */ -struct PagingLibraryView: View { +struct PagingLibraryView: View { @Default(.Customization.Library.enabledDrawerFilters) private var enabledDrawerFilters @@ -71,7 +71,7 @@ struct PagingLibraryView: View { private var posterType: PosterDisplayType @StateObject - private var collectionVGridProxy: CollectionVGridProxy = .init() + private var collectionVGridProxy: CollectionVGridProxy = .init() @StateObject private var viewModel: PagingLibraryViewModel @@ -239,8 +239,8 @@ struct PagingLibraryView: View { @ViewBuilder private var gridView: some View { CollectionVGrid( - $viewModel.elements, - layout: $layout + uniqueElements: viewModel.elements, + layout: layout ) { item in let displayType = Defaults[.Customization.Library.rememberLayout] ? _displayType.wrappedValue : _defaultDisplayType diff --git a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift index 3d5a8040..31b0969c 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift @@ -9,6 +9,7 @@ import CollectionHStack import Defaults import JellyfinAPI +import OrderedCollections import SwiftUI import VLCUI @@ -42,7 +43,7 @@ extension VideoPlayer.Overlay { private var size: CGSize = .zero @StateObject - private var collectionHStackProxy: CollectionHStackProxy = .init() + private var collectionHStackProxy: CollectionHStackProxy = .init() var body: some View { VStack(spacing: 0) { @@ -61,7 +62,7 @@ extension VideoPlayer.Overlay { Button { if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) { - collectionHStackProxy.scrollTo(element: currentChapter, animated: true) + collectionHStackProxy.scrollTo(element: currentChapter) } } label: { Text(L10n.current) @@ -73,7 +74,7 @@ extension VideoPlayer.Overlay { .edgePadding(.horizontal) CollectionHStack( - viewModel.chapters, + uniqueElements: viewModel.chapters, minWidth: 200 ) { chapter in ChapterButton(chapter: chapter)