diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift index 2e40fc88..98c541ab 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift @@ -27,6 +27,13 @@ extension BaseItemDto: LibraryParent { } } +extension BaseItemDto: LibraryIdentifiable { + + var unwrappedIDHashOrZero: Int { + id?.hashValue ?? 0 + } +} + extension BaseItemDto { var episodeLocator: String? { diff --git a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift index ae99cfc5..3c7c7044 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift @@ -13,6 +13,10 @@ import UIKit extension BaseItemPerson: Poster { + var unwrappedIDHashOrZero: Int { + id?.hashValue ?? 0 + } + var subtitle: String? { firstRole } diff --git a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift index a120292c..86de4199 100644 --- a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift @@ -45,6 +45,10 @@ extension ChapterInfo { chapterInfo.displayTitle } + var unwrappedIDHashOrZero: Int { + id + } + let systemImage: String = "film" var subtitle: String? var showTitle: Bool = true diff --git a/Shared/Objects/ChannelProgram.swift b/Shared/Objects/ChannelProgram.swift index f556495b..43dc567d 100644 --- a/Shared/Objects/ChannelProgram.swift +++ b/Shared/Objects/ChannelProgram.swift @@ -42,6 +42,10 @@ struct ChannelProgram: Hashable, Identifiable { extension ChannelProgram: Poster { + var unwrappedIDHashOrZero: Int { + channel.id?.hashValue ?? 0 + } + var displayTitle: String { channel.displayTitle } diff --git a/Shared/Objects/Poster.swift b/Shared/Objects/Poster.swift index 6d255a96..cdf18e0b 100644 --- a/Shared/Objects/Poster.swift +++ b/Shared/Objects/Poster.swift @@ -9,7 +9,7 @@ import Foundation /// A type that is displayed as a poster -protocol Poster: Displayable, Hashable, Identifiable, SystemImageable { +protocol Poster: Displayable, Hashable, LibraryIdentifiable, SystemImageable { /// Optional subtitle when used as a poster var subtitle: String? { get } diff --git a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift index abe775bc..4c82872e 100644 --- a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift @@ -18,6 +18,30 @@ import UIKit /// Magic number for page sizes private let DefaultPageSize = 50 +/// A protocol for items to conform to if they may be present within a library. +/// +/// Similar to `Identifiable`, but `unwrappedIDHashOrZero` is an `Int`: the hash of the underlying `id` +/// value if it is not optional, or if it is optional it must return the hash of the wrapped value, +/// or 0 otherwise: +/// +/// struct Item: LibraryIdentifiable { +/// var id: String? { "id" } +/// +/// var unwrappedIDHashOrZero: Int { +/// // Gets the `hashValue` of the `String.hashValue`, not `Optional.hashValue`. +/// id?.hashValue ?? 0 +/// } +/// } +/// +/// This is necessary because if the `ID` is optional, then `Optional.hashValue` will be used instead +/// and result in differing hashes. +/// +/// This also helps if items already conform to `Identifiable`, but has an optionally-typed `id`. +protocol LibraryIdentifiable: Identifiable { + + var unwrappedIDHashOrZero: Int { get } +} + // TODO: fix how `hasNextPage` is determined // - some subclasses might not have "paging" and only have one call. This can be solved with // a check if elements were actually appended to the set but that requires a redundant get @@ -33,7 +57,7 @@ private let DefaultPageSize = 50 on remembering other filters. */ -class PagingLibraryViewModel: ViewModel, Eventful, Stateful { +class PagingLibraryViewModel: ViewModel, Eventful, Stateful { // MARK: Event @@ -105,11 +129,21 @@ class PagingLibraryViewModel: ViewModel, Eventfu parent: (any LibraryParent)? = nil ) { self.filterViewModel = nil - self.elements = IdentifiedArray(uniqueElements: data, id: \.id.hashValue) + self.elements = IdentifiedArray(uniqueElements: data, id: \.unwrappedIDHashOrZero) self.isStatic = true self.hasNextPage = false self.pageSize = DefaultPageSize self.parent = parent + + super.init() + + Notifications[.didDeleteItem] + .publisher + .receive(on: RunLoop.main) + .sink { id in + self.elements.remove(id: id.hashValue) + } + .store(in: &cancellables) } convenience init( @@ -132,7 +166,7 @@ class PagingLibraryViewModel: ViewModel, Eventfu filters: ItemFilterCollection? = nil, pageSize: Int = DefaultPageSize ) { - self.elements = IdentifiedArray(id: \.id.hashValue) + self.elements = IdentifiedArray(id: \.unwrappedIDHashOrZero) self.isStatic = false self.pageSize = pageSize self.parent = parent diff --git a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift index 731f318d..b2e63a52 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 @@ -240,6 +240,7 @@ struct PagingLibraryView: View { private var gridView: some View { CollectionVGrid( uniqueElements: viewModel.elements, + id: \.unwrappedIDHashOrZero, layout: layout ) { item in