From 47397c5a5bbd538cc283c49674c0b34c084d0705 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Dec 2024 13:15:26 -0700 Subject: [PATCH 01/13] WIP --- .../Coordinators/ItemEditorCoordinator.swift | 13 + .../JellyfinAPI/RemoteSearchResult.swift | 33 +++ Shared/Strings/Strings.swift | 4 +- .../ItemInfoViewModel.swift | 168 ++++++++++++ .../MovieInfoViewModel.swift | 31 +++ .../SeriesInfoViewModel.swift | 31 +++ Swiftfin.xcodeproj/project.pbxproj | 60 +++++ .../Components/ItemInfoConfirmationView.swift | 66 +++++ .../Components/ItemInfoResultButton.swift | 63 +++++ .../IdentifyItemView/IdentifyItemView.swift | 252 ++++++++++++++++++ .../Views/ItemEditorView/ItemEditorView.swift | 4 + 11 files changed, 723 insertions(+), 2 deletions(-) create mode 100644 Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift create mode 100644 Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/ItemInfoViewModel.swift create mode 100644 Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/MovieInfoViewModel.swift create mode 100644 Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/SeriesInfoViewModel.swift create mode 100644 Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift create mode 100644 Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift create mode 100644 Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 37e698eac..6325554c4 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -21,6 +21,8 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Route to Metadata + @Route(.modal) + var editIdentity = makeEditIdentity @Route(.modal) var editMetadata = makeEditMetadata @@ -60,6 +62,17 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Item Metadata + func makeEditIdentity(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator { + switch item.type { + case .movie: + IdentifyItemView(viewModel: MovieInfoViewModel(item: item)) + default: + ErrorView(error: JellyfinAPIError("How did you get here?")) + } + } + } + func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator { NavigationViewCoordinator { EditMetadataView(viewModel: ItemEditorViewModel(item: item)) diff --git a/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift new file mode 100644 index 000000000..b574f143a --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift @@ -0,0 +1,33 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import SwiftUI + +extension RemoteSearchResult: @retroactive Equatable, @retroactive Identifiable { + + public var id: String { + UUID().uuidString + } + + public static func == (lhs: RemoteSearchResult, rhs: RemoteSearchResult) -> Bool { + lhs.albumArtist == rhs.albumArtist && + lhs.artists == rhs.artists && + lhs.imageURL == rhs.imageURL && + lhs.indexNumber == rhs.indexNumber && + lhs.indexNumberEnd == rhs.indexNumberEnd && + lhs.name == rhs.name && + lhs.overview == rhs.overview && + lhs.parentIndexNumber == rhs.parentIndexNumber && + lhs.premiereDate == rhs.premiereDate && + lhs.productionYear == rhs.productionYear && + lhs.providerIDs == rhs.providerIDs && + lhs.searchProviderName == rhs.searchProviderName + } +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index eb61fe726..472beedb9 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -46,8 +46,8 @@ internal enum L10n { internal static let additionalSecurityAccessDescription = L10n.tr("Localizable", "additionalSecurityAccessDescription", fallback: "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.") /// Add Server internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server") - /// Add Trigger - internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add Trigger") + /// Add trigger + internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add trigger") /// Add URL internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL") /// Add User diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/ItemInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/ItemInfoViewModel.swift new file mode 100644 index 000000000..8cba1a9ff --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/ItemInfoViewModel.swift @@ -0,0 +1,168 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import Get +import JellyfinAPI +import OrderedCollections + +class ItemInfoViewModel: ViewModel, Stateful, Eventful { + + // MARK: - Events + + enum Event: Equatable { + case updated + case error(JellyfinAPIError) + } + + // MARK: - Actions + + enum Action: Equatable { + case search(SearchInfo) + case update(RemoteSearchResult) + } + + // MARK: BackgroundState + + enum BackgroundState: Hashable { + case searching + case refreshing + } + + // MARK: - State + + enum State: Hashable { + case initial + case updating + } + + @Published + var backgroundStates: OrderedSet = [] + @Published + var item: BaseItemDto + @Published + var searchResults: [RemoteSearchResult] = [] + @Published + var state: State = .initial + + private var updateTask: AnyCancellable? + private var searchTask: AnyCancellable? + + private let eventSubject = PassthroughSubject() + + var events: AnyPublisher { + eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher() + } + + // MARK: - Initializer + + init(item: BaseItemDto) { + self.item = item + super.init() + } + + // MARK: - Respond to Actions + + func respond(to action: Action) -> State { + switch action { + + case let .search(searchInfo): + searchTask?.cancel() + + searchTask = Task { [weak self] in + guard let self else { return } + + do { + await MainActor.run { + _ = self.backgroundStates.append(.searching) + } + + let allElements = try await self.searchItem(searchInfo) + + await MainActor.run { + self.searchResults = allElements + _ = self.backgroundStates.remove(.searching) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .initial + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + return state + + case let .update(searchResult): + updateTask?.cancel() + + updateTask = Task { [weak self] in + guard let self else { return } + + do { + await MainActor.run { + self.state = .updating + } + + try await updateItem(searchResult) + + await MainActor.run { + self.state = .initial + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .initial + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + + return state + } + } + + // MARK: - Return Matching Elements (To Be Overridden) + + func searchItem(_ searchInfo: SearchInfo) async throws -> [RemoteSearchResult] { + fatalError("This method should be overridden in subclasses") + } + + // MARK: - Save Updated Item to Server + + private func updateItem(_ match: RemoteSearchResult) async throws { + guard let itemId = item.id else { return } + + let request = Paths.applySearchCriteria(itemID: itemId, match) + _ = try await userSession.client.send(request) + + try await refreshItem() + } + + // MARK: - Refresh Item + + private func refreshItem() async throws { + guard let itemId = item.id else { return } + + await MainActor.run { + _ = self.backgroundStates.append(.refreshing) + } + + let request = Paths.getItem(userID: userSession.user.id, itemID: itemId) + let response = try await userSession.client.send(request) + + await MainActor.run { + self.item = response.value + _ = self.backgroundStates.remove(.refreshing) + + Notifications[.itemMetadataDidChange].post(item) + } + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/MovieInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/MovieInfoViewModel.swift new file mode 100644 index 000000000..21637e9f8 --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/MovieInfoViewModel.swift @@ -0,0 +1,31 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +class MovieInfoViewModel: ItemInfoViewModel { + + // MARK: - Return Matching Movies + + override func searchItem(_ movieInfo: MovieInfo) async throws -> [RemoteSearchResult] { + guard let itemId = item.id, item.type == .movie else { + return [] + } + + let parameters = MovieInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: movieInfo + ) + let request = Paths.getMovieRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/SeriesInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/SeriesInfoViewModel.swift new file mode 100644 index 000000000..5deb185c3 --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/SeriesInfoViewModel.swift @@ -0,0 +1,31 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +class SeriesInfoViewModel: ItemInfoViewModel { + + // MARK: - Return Matching Movies + + override func searchItem(_ seriesInfo: SeriesInfo) async throws -> [RemoteSearchResult] { + guard let itemId = item.id, item.type == .movie else { + return [] + } + + let parameters = SeriesInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: seriesInfo + ) + let request = Paths.getSeriesRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index f151b3736..e584efaf4 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -208,6 +208,17 @@ 4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; }; + 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */; }; + 4EE766F72D132054009658F0 /* ItemInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */; }; + 4EE766F82D132054009658F0 /* ItemInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */; }; + 4EE766FA2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; + 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; + 4EE766FE2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */; }; + 4EE766FF2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */; }; + 4EE767012D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */; }; + 4EE767022D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */; }; + 4EE767082D13403F009658F0 /* ItemInfoResultButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */; }; + 4EE7670A2D135CBA009658F0 /* ItemInfoConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */; }; 4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; }; 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; }; 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; }; @@ -1325,6 +1336,13 @@ 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = ""; }; + 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemView.swift; sourceTree = ""; }; + 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoViewModel.swift; sourceTree = ""; }; + 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResult.swift; sourceTree = ""; }; + 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfoViewModel.swift; sourceTree = ""; }; + 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesInfoViewModel.swift; sourceTree = ""; }; + 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoResultButton.swift; sourceTree = ""; }; + 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoConfirmationView.swift; sourceTree = ""; }; 4EED87462CBF824B002354D2 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = ""; }; 4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; }; @@ -2446,6 +2464,7 @@ 4E8F74A62CE03D4C00CC8969 /* Components */, 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, 4E6619FF2CEFE39000025C99 /* EditMetadataView */, + 4EE766F32D131F6E009658F0 /* IdentifyItemView */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, ); path = ItemEditorView; @@ -2463,6 +2482,7 @@ isa = PBXGroup; children = ( 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, + 4EE766FC2D132CDB009658F0 /* ItemIdentificationViewModel */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, ); @@ -2688,6 +2708,34 @@ path = EditAccessScheduleView; sourceTree = ""; }; + 4EE766F32D131F6E009658F0 /* IdentifyItemView */ = { + isa = PBXGroup; + children = ( + 4EE767062D13401C009658F0 /* Components */, + 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */, + ); + path = IdentifyItemView; + sourceTree = ""; + }; + 4EE766FC2D132CDB009658F0 /* ItemIdentificationViewModel */ = { + isa = PBXGroup; + children = ( + 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */, + 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */, + 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */, + ); + path = ItemIdentificationViewModel; + sourceTree = ""; + }; + 4EE767062D13401C009658F0 /* Components */ = { + isa = PBXGroup; + children = ( + 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */, + 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */, + ); + path = Components; + sourceTree = ""; + }; 4EED87472CBF824B002354D2 /* Components */ = { isa = PBXGroup; children = ( @@ -4308,6 +4356,7 @@ 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */, E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */, 4E2182E42CAF67EF0094806B /* PlayMethod.swift */, + 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */, 4E35CE652CBED8B300DBD886 /* ServerTicks.swift */, 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */, 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */, @@ -5044,6 +5093,7 @@ E1575E92293E7B1E001665B1 /* CGSize.swift in Sources */, E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */, C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */, + 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */, C46DD8EA2A8FB45C0046A504 /* LiveOverlay.swift in Sources */, E11E376D293E9CC1009EF240 /* VideoPlayerCoordinator.swift in Sources */, E1575E6F293E77B5001665B1 /* GestureAction.swift in Sources */, @@ -5095,6 +5145,7 @@ E1E9EFEB28C7EA2C00CC1F8B /* UserDto.swift in Sources */, E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */, 4E2AC4C32C6C491200DD600D /* AudoCodec.swift in Sources */, + 4EE766FF2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */, E1575EA6293E7D40001665B1 /* VideoPlayer.swift in Sources */, E185920628CDAA6400326F80 /* CastAndCrewHStack.swift in Sources */, E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */, @@ -5133,6 +5184,7 @@ E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */, E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */, E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */, + 4EE766F82D132054009658F0 /* ItemInfoViewModel.swift in Sources */, E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */, E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */, BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */, @@ -5256,6 +5308,7 @@ E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, E1DABAFC2A270EE7008AC34A /* MediaSourcesCard.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, + 4EE767022D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */, E14EDEC62B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */, E14EDEC92B8FB65F000F00A4 /* ItemFilterType.swift in Sources */, E1D37F4C2B9CEA5C00343D2B /* ImageSource.swift in Sources */, @@ -5548,6 +5601,7 @@ 4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */, 4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */, 4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */, + 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */, 4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */, 4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */, 4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */, @@ -5558,6 +5612,7 @@ E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */, + 4EE767082D13403F009658F0 /* ItemInfoResultButton.swift in Sources */, E1ED7FD92CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */, E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */, 4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */, @@ -5581,6 +5636,7 @@ E111D8F828D03BF900400001 /* PagingLibraryView.swift in Sources */, E187F7672B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, 4E10C8172CC0455A0012CC9F /* CompatibilitiesSection.swift in Sources */, + 4EE7670A2D135CBA009658F0 /* ItemInfoConfirmationView.swift in Sources */, E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */, E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */, @@ -5831,6 +5887,7 @@ 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */, E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, + 4EE767012D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */, E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */, 4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */, 4E537A842D03D11200659A1A /* ServerUserDeviceAccessView.swift in Sources */, @@ -5938,6 +5995,7 @@ 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */, E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */, + 4EE766FE2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */, 4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */, E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */, E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */, @@ -5957,6 +6015,7 @@ E1AD105F26D9ADDD003E4A08 /* NameGuidPair.swift in Sources */, 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */, E18A8E7D28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, + 4EE766F72D132054009658F0 /* ItemInfoViewModel.swift in Sources */, 4EC2B19B2CC96E7400D866BE /* ServerUsersView.swift in Sources */, E18E01F1288747230022598C /* PlayButton.swift in Sources */, E129429028F0BDC300796AC6 /* TimeStampType.swift in Sources */, @@ -5977,6 +6036,7 @@ E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, E111D8F528D03B7500400001 /* PagingLibraryViewModel.swift in Sources */, E16AF11C292C98A7001422A8 /* GestureSettingsView.swift in Sources */, + 4EE766FA2D132954009658F0 /* RemoteSearchResult.swift in Sources */, E1581E27291EF59800D6C640 /* SplitContentView.swift in Sources */, C46DD8DC2A8DC3420046A504 /* LiveVideoPlayer.swift in Sources */, E11BDF972B865F550045C54A /* ItemTag.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift new file mode 100644 index 000000000..64e6ec80f --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift @@ -0,0 +1,66 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension IdentifyItemView { + + struct ItemInfoConfirmationView: View { + + // MARK: - Item Info Variables + + let itemInfo: RemoteSearchResult + let remoteImage: any View + + // MARK: - Item Info Actions + + let onSave: () -> Void + let onClose: () -> Void + + // MARK: - Body + + var body: some View { + NavigationView { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top) { + remoteImage + .eraseToAnyView() + .frame(width: 60, height: 180) + .padding(.horizontal, 32) + + Spacer() + } + + Text(itemInfo.premiereDate?.formatted(.dateTime.year().month().day()) ?? .emptyDash) + .foregroundStyle(Color.primary) + + Text(itemInfo.overview ?? L10n.unknown) + .foregroundStyle(Color.secondary) + + Spacer() + + Text(itemInfo.searchProviderName ?? L10n.unknown) + .foregroundStyle(Color.secondary) + .frame(maxWidth: .infinity, alignment: .center) + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(itemInfo.name ?? L10n.unknown) + .navigationBarCloseButton { + onClose() + } + .topBarTrailing { + Button(L10n.save) { + onSave() + } + .buttonStyle(.toolbarPill) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift new file mode 100644 index 000000000..8a300d3b6 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift @@ -0,0 +1,63 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension IdentifyItemView { + + struct RemoteSearchResultButton: View { + + // MARK: - Remote Search Result Variable + + let remoteSearchResult: RemoteSearchResult + let remoteImage: any View + + // MARK: - Remote Search Result Action + + let onSelect: () -> Void + + // MARK: - Result Title + + private var resultTitle: String { + let name = remoteSearchResult.name ?? L10n.unknown + let year = remoteSearchResult.productionYear?.description ?? .emptyDash + + return "\(name) (\(year))" + } + + // MARK: - Body + + var body: some View { + Button(action: onSelect) { + HStack { + remoteImage + .eraseToAnyView() + .frame(width: 30, height: 90) + .padding(.horizontal) + + VStack(alignment: .leading) { + Text( + resultTitle + ) + .font(.headline) + .foregroundStyle(Color.primary) + + HStack { + Text(remoteSearchResult.overview ?? L10n.unknown) + .lineLimit(3) + .font(.subheadline) + .foregroundStyle(Color.secondary) + } + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift new file mode 100644 index 000000000..dc206840e --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -0,0 +1,252 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +struct IdentifyItemView: View { + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + private var viewModel: ItemInfoViewModel + + // MARK: - Identity Variables + + @State + private var tempSearchInfo: SearchInfo? = nil + @State + private var selectedMatch: RemoteSearchResult? = nil + + // MARK: - State Variables for Search Info + + @State + private var movieInfo = MovieInfo() + @State + private var seriesInfo = SeriesInfo() + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Initializer + + init(viewModel: ItemInfoViewModel) { + self.viewModel = viewModel + } + + // MARK: - Body + + @ViewBuilder + var body: some View { + contentView + .navigationBarTitle(L10n.metadata) + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: Binding( + get: { selectedMatch != nil }, + set: { if !$0 { selectedMatch = nil } } + )) { + if let selectedMatch { + selectionConfirmationModal(selectedMatch) + } + } + .topBarTrailing { + Button(L10n.search) { + if let tempSearchInfo { + viewModel.send(.search(tempSearchInfo)) + } + } + .buttonStyle(.toolbarPill) + } + .navigationBarCloseButton { + router.dismissCoordinator() + } + .onReceive(viewModel.events) { events in + switch events { + case let .error(eventError): + error = eventError + default: + break + } + } + .errorMessage($error) + } + + @ViewBuilder + var contentView: some View { + Form { + switch viewModel.item.type { + case .movie: + movieSearch + case .series: + seriesSearch + default: + EmptyView() + } + searchResultsView + } + } + + // MARK: - Search Results + + @ViewBuilder + private var searchResultsView: some View { + if viewModel.searchResults.isNotEmpty { + Section(L10n.items) { + ForEach(viewModel.searchResults, id: \.id) { remoteSearchResult in + RemoteSearchResultButton( + remoteSearchResult: remoteSearchResult, + remoteImage: resultImage(remoteSearchResult.imageURL) + ) { + selectedMatch = remoteSearchResult + } + } + } + } + } + + // MARK: - Movie Search Fields + + @ViewBuilder + private var movieSearch: some View { + Section(header: Text(L10n.search)) { + TextField( + L10n.name, + text: Binding( + get: { movieInfo.name ?? "" }, + set: { newValue in + movieInfo.name = newValue + tempSearchInfo = movieInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.originalTitle, + text: Binding( + get: { movieInfo.originalTitle ?? "" }, + set: { newValue in + movieInfo.originalTitle = newValue + tempSearchInfo = movieInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.year, + text: Binding( + get: { movieInfo.year.map(String.init) ?? "" }, + set: { newValue in + movieInfo.year = Int(newValue) + tempSearchInfo = movieInfo as? SearchInfo + } + ) + ) + .keyboardType(.numberPad) + } + } + + // MARK: - Series Search Fields + + @ViewBuilder + private var seriesSearch: some View { + Section(header: Text(L10n.search)) { + TextField( + L10n.name, + text: Binding( + get: { seriesInfo.name ?? "" }, + set: { newValue in + seriesInfo.name = newValue + tempSearchInfo = seriesInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.originalTitle, + text: Binding( + get: { seriesInfo.originalTitle ?? "" }, + set: { newValue in + seriesInfo.originalTitle = newValue + tempSearchInfo = seriesInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.year, + text: Binding( + get: { seriesInfo.year.map(String.init) ?? "" }, + set: { newValue in + seriesInfo.year = Int(newValue) + tempSearchInfo = seriesInfo as? SearchInfo + } + ) + ) + .keyboardType(.numberPad) + } + } + + // MARK: - Selection Confirmation Modal + + @ViewBuilder + private func selectionConfirmationModal(_ selected: RemoteSearchResult) -> some View { + NavigationView { + VStack(alignment: .leading) { + resultImage(selected.imageURL) + .frame(width: 60, height: 180) + .padding(.leading) + .padding() + Text(selected.premiereDate?.formatted(.dateTime.year().month().day()) ?? .emptyDash) + .foregroundStyle(Color.primary) + .padding() + Text(selected.overview ?? L10n.unknown) + .foregroundStyle(Color.secondary) + .padding() + Text(selected.searchProviderName ?? L10n.unknown) + .foregroundStyle(Color.secondary) + .frame(maxWidth: .infinity, alignment: .center) + .padding() + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(selected.name ?? L10n.unknown) + .navigationBarCloseButton { + selectedMatch = nil + } + .topBarTrailing { + Button(L10n.save) { + viewModel.send(.update(selected)) + selectedMatch = nil + router.dismissCoordinator() + } + .buttonStyle(.toolbarPill) + } + } + } + + // MARK: - Result Image + + @ViewBuilder + public func resultImage(_ url: String? = nil) -> some View { + ZStack { + Color.clear + + ImageView(URL(string: url ?? "")) + .failure { + SystemImageContentView(systemName: "questionmark") + } + } + .posterStyle(.portrait) + .posterShadow() + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index f926dd576..63cf5bd5c 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -98,6 +98,10 @@ struct ItemEditorView: View { @ViewBuilder private var editView: some View { Section(L10n.edit) { + ChevronButton("Identify") + .onSelect { + router.route(to: \.editIdentity, viewModel.item) + } ChevronButton(L10n.metadata) .onSelect { router.route(to: \.editMetadata, viewModel.item) From cb0e34fe58b17d5c281c3de3b9815b570cb95e32 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Dec 2024 15:11:02 -0700 Subject: [PATCH 02/13] All item types. --- .../BoxSetInfoViewModel.swift | 31 +++ .../ItemInfoViewModel.swift | 0 .../MovieInfoViewModel.swift | 0 .../PersonInfoViewModel.swift | 31 +++ .../SeriesInfoViewModel.swift | 4 +- Swiftfin.xcodeproj/project.pbxproj | 20 +- .../IdentifyItemView/IdentifyItemView.swift | 186 ++++++++++++++---- 7 files changed, 227 insertions(+), 45 deletions(-) create mode 100644 Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift rename Shared/ViewModels/ItemAdministration/{ItemIdentificationViewModel => ItemInfoViewModel}/ItemInfoViewModel.swift (100%) rename Shared/ViewModels/ItemAdministration/{ItemIdentificationViewModel => ItemInfoViewModel}/MovieInfoViewModel.swift (100%) create mode 100644 Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift rename Shared/ViewModels/ItemAdministration/{ItemIdentificationViewModel => ItemInfoViewModel}/SeriesInfoViewModel.swift (88%) diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift new file mode 100644 index 000000000..018eec8d7 --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift @@ -0,0 +1,31 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +class BoxSetInfoViewModel: ItemInfoViewModel { + + // MARK: - Return Matching Box Set + + override func searchItem(_ boxSetInfo: BoxSetInfo) async throws -> [RemoteSearchResult] { + guard let itemId = item.id, item.type == .boxSet else { + return [] + } + + let parameters = BoxSetInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: boxSetInfo + ) + let request = Paths.getBoxSetRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/ItemInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift similarity index 100% rename from Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/ItemInfoViewModel.swift rename to Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/MovieInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/MovieInfoViewModel.swift similarity index 100% rename from Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/MovieInfoViewModel.swift rename to Shared/ViewModels/ItemAdministration/ItemInfoViewModel/MovieInfoViewModel.swift diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift new file mode 100644 index 000000000..4bd76d8fd --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift @@ -0,0 +1,31 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +class PersonInfoViewModel: ItemInfoViewModel { + + // MARK: - Return Matching People + + override func searchItem(_ personLookupInfo: PersonLookupInfo) async throws -> [RemoteSearchResult] { + guard let itemId = item.id, item.type == .person else { + return [] + } + + let parameters = PersonLookupInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: personLookupInfo + ) + let request = Paths.getPersonRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/SeriesInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/SeriesInfoViewModel.swift similarity index 88% rename from Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/SeriesInfoViewModel.swift rename to Shared/ViewModels/ItemAdministration/ItemInfoViewModel/SeriesInfoViewModel.swift index 5deb185c3..79910e4e7 100644 --- a/Shared/ViewModels/ItemAdministration/ItemIdentificationViewModel/SeriesInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/SeriesInfoViewModel.swift @@ -12,10 +12,10 @@ import JellyfinAPI class SeriesInfoViewModel: ItemInfoViewModel { - // MARK: - Return Matching Movies + // MARK: - Return Matching Series override func searchItem(_ seriesInfo: SeriesInfo) async throws -> [RemoteSearchResult] { - guard let itemId = item.id, item.type == .movie else { + guard let itemId = item.id, item.type == .series else { return [] } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index e584efaf4..cfc3e65a5 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -219,6 +219,10 @@ 4EE767022D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */; }; 4EE767082D13403F009658F0 /* ItemInfoResultButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */; }; 4EE7670A2D135CBA009658F0 /* ItemInfoConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */; }; + 4EE7670C2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */; }; + 4EE7670D2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */; }; + 4EE7670F2D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */; }; + 4EE767102D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */; }; 4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; }; 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; }; 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; }; @@ -1343,6 +1347,8 @@ 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesInfoViewModel.swift; sourceTree = ""; }; 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoResultButton.swift; sourceTree = ""; }; 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoConfirmationView.swift; sourceTree = ""; }; + 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonInfoViewModel.swift; sourceTree = ""; }; + 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxSetInfoViewModel.swift; sourceTree = ""; }; 4EED87462CBF824B002354D2 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = ""; }; 4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; }; @@ -2482,7 +2488,7 @@ isa = PBXGroup; children = ( 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, - 4EE766FC2D132CDB009658F0 /* ItemIdentificationViewModel */, + 4EE766FC2D132CDB009658F0 /* ItemInfoViewModel */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, ); @@ -2717,14 +2723,16 @@ path = IdentifyItemView; sourceTree = ""; }; - 4EE766FC2D132CDB009658F0 /* ItemIdentificationViewModel */ = { + 4EE766FC2D132CDB009658F0 /* ItemInfoViewModel */ = { isa = PBXGroup; children = ( + 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */, 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */, - 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */, 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */, + 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */, + 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */, ); - path = ItemIdentificationViewModel; + path = ItemInfoViewModel; sourceTree = ""; }; 4EE767062D13401C009658F0 /* Components */ = { @@ -5158,6 +5166,7 @@ E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */, E1575E85293E7A00001665B1 /* DarkAppIcon.swift in Sources */, 4E49DED22CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */, + 4EE767102D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */, E1763A762BF3FF01004DF6AB /* AppLoadingView.swift in Sources */, E18121062CBE428000682985 /* ChevronButton.swift in Sources */, E102315A2BCF8AF8009D71FC /* ProgramButtonContent.swift in Sources */, @@ -5325,6 +5334,7 @@ E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */, E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */, E1ED7FE22CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, + 4EE7670D2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */, E1CB75762C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E102315B2BCF8AF8009D71FC /* ProgramProgressOverlay.swift in Sources */, E1E6C45129B104850064123F /* Button.swift in Sources */, @@ -5594,6 +5604,7 @@ E18E01FA288747580022598C /* AboutAppView.swift in Sources */, E170D103294CE8BF0017224C /* LoadingView.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, + 4EE7670C2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */, E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */, E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */, E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, @@ -5836,6 +5847,7 @@ 4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */, 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */, E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */, + 4EE7670F2D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */, E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */, E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */, E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift index dc206840e..07beefb62 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -27,17 +27,21 @@ struct IdentifyItemView: View { @State private var selectedMatch: RemoteSearchResult? = nil - // MARK: - State Variables for Search Info + // MARK: - Error State @State - private var movieInfo = MovieInfo() - @State - private var seriesInfo = SeriesInfo() + private var error: Error? - // MARK: - Error State + // MARK: - Potential States @State - private var error: Error? + var boxSetInfo = BoxSetInfo() + @State + var movieInfo = MovieInfo() + @State + var personInfo = PersonLookupInfo() + @State + var seriesInfo = SeriesInfo() // MARK: - Initializer @@ -49,47 +53,70 @@ struct IdentifyItemView: View { @ViewBuilder var body: some View { - contentView - .navigationBarTitle(L10n.metadata) - .navigationBarTitleDisplayMode(.inline) - .sheet(isPresented: Binding( - get: { selectedMatch != nil }, - set: { if !$0 { selectedMatch = nil } } - )) { - if let selectedMatch { - selectionConfirmationModal(selectedMatch) - } + Group { + switch viewModel.state { + case .updating: + updateView + case .initial: + contentView } - .topBarTrailing { - Button(L10n.search) { - if let tempSearchInfo { - viewModel.send(.search(tempSearchInfo)) - } + } + .navigationBarTitle(L10n.metadata) + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: Binding( + get: { selectedMatch != nil }, + set: { if !$0 { selectedMatch = nil } } + )) { + if let selectedMatch { + selectionConfirmationModal(selectedMatch) + } + } + .topBarTrailing { + Button(L10n.search) { + if let tempSearchInfo { + viewModel.send(.search(tempSearchInfo)) } - .buttonStyle(.toolbarPill) } - .navigationBarCloseButton { + .buttonStyle(.toolbarPill) + } + .navigationBarCloseButton { + router.dismissCoordinator() + } + .onReceive(viewModel.events) { events in + switch events { + case let .error(eventError): + error = eventError + case .updated: router.dismissCoordinator() } - .onReceive(viewModel.events) { events in - switch events { - case let .error(eventError): - error = eventError - default: - break - } - } - .errorMessage($error) + } + .errorMessage($error) } + // MARK: - UpdatE View + + @ViewBuilder + var updateView: some View { + VStack(alignment: .center) { + Text(selectedMatch?.name ?? L10n.unknown) + ProgressView() + } + } + + // MARK: - Content View + @ViewBuilder var contentView: some View { Form { switch viewModel.item.type { + case .boxSet: + boxSetSearchView case .movie: - movieSearch + movieSearchView + case .person: + personSearchView case .series: - seriesSearch + seriesSearchView default: EmptyView() } @@ -115,10 +142,51 @@ struct IdentifyItemView: View { } } - // MARK: - Movie Search Fields + // MARK: - Box Set Search View + + @ViewBuilder + private var boxSetSearchView: some View { + Section(header: Text(L10n.search)) { + TextField( + L10n.name, + text: Binding( + get: { boxSetInfo.name ?? "" }, + set: { newValue in + boxSetInfo.name = newValue + tempSearchInfo = boxSetInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.originalTitle, + text: Binding( + get: { boxSetInfo.originalTitle ?? "" }, + set: { newValue in + boxSetInfo.originalTitle = newValue + tempSearchInfo = boxSetInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.year, + text: Binding( + get: { boxSetInfo.year.map(String.init) ?? "" }, + set: { newValue in + boxSetInfo.year = Int(newValue) + tempSearchInfo = boxSetInfo as? SearchInfo + } + ) + ) + .keyboardType(.numberPad) + } + } + + // MARK: - Movie Search View @ViewBuilder - private var movieSearch: some View { + private var movieSearchView: some View { Section(header: Text(L10n.search)) { TextField( L10n.name, @@ -156,10 +224,51 @@ struct IdentifyItemView: View { } } - // MARK: - Series Search Fields + // MARK: - Person Search View + + @ViewBuilder + private var personSearchView: some View { + Section(header: Text(L10n.search)) { + TextField( + L10n.name, + text: Binding( + get: { personInfo.name ?? "" }, + set: { newValue in + personInfo.name = newValue + tempSearchInfo = personInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.originalTitle, + text: Binding( + get: { personInfo.originalTitle ?? "" }, + set: { newValue in + personInfo.originalTitle = newValue + tempSearchInfo = personInfo as? SearchInfo + } + ) + ) + + TextField( + L10n.year, + text: Binding( + get: { personInfo.year.map(String.init) ?? "" }, + set: { newValue in + personInfo.year = Int(newValue) + tempSearchInfo = personInfo as? SearchInfo + } + ) + ) + .keyboardType(.numberPad) + } + } + + // MARK: - Series Search View @ViewBuilder - private var seriesSearch: some View { + private var seriesSearchView: some View { Section(header: Text(L10n.search)) { TextField( L10n.name, @@ -227,7 +336,6 @@ struct IdentifyItemView: View { Button(L10n.save) { viewModel.send(.update(selected)) selectedMatch = nil - router.dismissCoordinator() } .buttonStyle(.toolbarPill) } From 00f190b90e4d9c9db296f96fac25da3bb1c4ee92 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Dec 2024 22:35:07 -0700 Subject: [PATCH 03/13] V2: Functionally better. UI still weird --- .../Coordinators/ItemEditorCoordinator.swift | 23 +++--- .../ItemInfoViewModel/ItemInfoViewModel.swift | 11 +++ .../Components/ItemInfoConfirmationView.swift | 35 ++++----- .../Components/ItemInfoResultButton.swift | 4 +- .../IdentifyItemView/IdentifyItemView.swift | 72 +++++++------------ .../Views/ItemEditorView/ItemEditorView.swift | 10 +-- 6 files changed, 78 insertions(+), 77 deletions(-) diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 6325554c4..312302b42 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -21,7 +21,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Route to Metadata - @Route(.modal) + @Route(.push) var editIdentity = makeEditIdentity @Route(.modal) var editMetadata = makeEditMetadata @@ -62,14 +62,19 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Item Metadata - func makeEditIdentity(item: BaseItemDto) -> NavigationViewCoordinator { - NavigationViewCoordinator { - switch item.type { - case .movie: - IdentifyItemView(viewModel: MovieInfoViewModel(item: item)) - default: - ErrorView(error: JellyfinAPIError("How did you get here?")) - } + @ViewBuilder + func makeEditIdentity(item: BaseItemDto) -> some View { + switch item.type { + case .boxSet: + IdentifyItemView(viewModel: BoxSetInfoViewModel(item: item)) + case .movie: + IdentifyItemView(viewModel: MovieInfoViewModel(item: item)) + case .person: + IdentifyItemView(viewModel: PersonInfoViewModel(item: item)) + case .series: + IdentifyItemView(viewModel: SeriesInfoViewModel(item: item)) + default: + ErrorView(error: JellyfinAPIError("Invalid media type")) } } diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift index 8cba1a9ff..0901a5a31 100644 --- a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift @@ -18,12 +18,14 @@ class ItemInfoViewModel: ViewModel, Stateful, Eventful { enum Event: Equatable { case updated + case cancelled case error(JellyfinAPIError) } // MARK: - Actions enum Action: Equatable { + case cancel case search(SearchInfo) case update(RemoteSearchResult) } @@ -72,6 +74,15 @@ class ItemInfoViewModel: ViewModel, Stateful, Eventful { func respond(to action: Action) -> State { switch action { + case .cancel: + updateTask?.cancel() + searchTask?.cancel() + + self.backgroundStates = [] + self.state = .initial + + return state + case let .search(searchInfo): searchTask?.cancel() diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift index 64e6ec80f..a198cda16 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift @@ -28,17 +28,17 @@ extension IdentifyItemView { var body: some View { NavigationView { VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .top) { - remoteImage - .eraseToAnyView() - .frame(width: 60, height: 180) - .padding(.horizontal, 32) + remoteImage + .eraseToAnyView() + .frame(width: 60, height: 180, alignment: .leading) - Spacer() - } + Text(itemInfo.name ?? L10n.unknown) + .foregroundStyle(Color.primary) + .font(.headline) Text(itemInfo.premiereDate?.formatted(.dateTime.year().month().day()) ?? .emptyDash) .foregroundStyle(Color.primary) + .font(.subheadline) Text(itemInfo.overview ?? L10n.unknown) .foregroundStyle(Color.secondary) @@ -49,17 +49,18 @@ extension IdentifyItemView { .foregroundStyle(Color.secondary) .frame(maxWidth: .infinity, alignment: .center) } - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(itemInfo.name ?? L10n.unknown) - .navigationBarCloseButton { - onClose() - } - .topBarTrailing { - Button(L10n.save) { - onSave() + .padding() + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Item") + .navigationBarCloseButton { + onClose() + } + .topBarTrailing { + Button(L10n.save) { + onSave() + } + .buttonStyle(.toolbarPill) } - .buttonStyle(.toolbarPill) } } } diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift index 8a300d3b6..363126217 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift @@ -27,7 +27,9 @@ extension IdentifyItemView { private var resultTitle: String { let name = remoteSearchResult.name ?? L10n.unknown - let year = remoteSearchResult.productionYear?.description ?? .emptyDash + let year = remoteSearchResult.productionYear?.description ?? + remoteSearchResult.premiereDate?.formatted(.dateTime.year()).description ?? + .emptyDash return "\(name) (\(year))" } diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift index 07beefb62..c1493c8d2 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -15,7 +15,7 @@ struct IdentifyItemView: View { // MARK: - Observed & Environment Objects @EnvironmentObject - private var router: BasicNavigationViewCoordinator.Router + private var router: ItemEditorCoordinator.Router @ObservedObject private var viewModel: ItemInfoViewModel @@ -23,9 +23,11 @@ struct IdentifyItemView: View { // MARK: - Identity Variables @State - private var tempSearchInfo: SearchInfo? = nil + private var lastSearch: SearchInfo? @State - private var selectedMatch: RemoteSearchResult? = nil + private var tempSearchInfo: SearchInfo? + @State + private var selectedMatch: RemoteSearchResult? // MARK: - Error State @@ -67,27 +69,37 @@ struct IdentifyItemView: View { get: { selectedMatch != nil }, set: { if !$0 { selectedMatch = nil } } )) { - if let selectedMatch { - selectionConfirmationModal(selectedMatch) + if let match = selectedMatch { + ItemInfoConfirmationView( + itemInfo: match, + remoteImage: resultImage(match.imageURL) + ) { + viewModel.send(.update(match)) + selectedMatch = nil + } onClose: { + selectedMatch = nil + } } } .topBarTrailing { Button(L10n.search) { if let tempSearchInfo { + lastSearch = tempSearchInfo viewModel.send(.search(tempSearchInfo)) } } .buttonStyle(.toolbarPill) + .disabled(viewModel.state == .updating || tempSearchInfo == nil || lastSearch == tempSearchInfo) } - .navigationBarCloseButton { - router.dismissCoordinator() - } + .navigationBarBackButtonHidden(viewModel.state == .updating) .onReceive(viewModel.events) { events in switch events { case let .error(eventError): error = eventError + case .cancelled: + selectedMatch = nil case .updated: - router.dismissCoordinator() + router.pop() } } .errorMessage($error) @@ -98,8 +110,12 @@ struct IdentifyItemView: View { @ViewBuilder var updateView: some View { VStack(alignment: .center) { - Text(selectedMatch?.name ?? L10n.unknown) ProgressView() + Button(L10n.cancel, role: .destructive) { + viewModel.send(.cancel) + } + .buttonStyle(.borderedProminent) + .tint(.red) } } @@ -306,42 +322,6 @@ struct IdentifyItemView: View { } } - // MARK: - Selection Confirmation Modal - - @ViewBuilder - private func selectionConfirmationModal(_ selected: RemoteSearchResult) -> some View { - NavigationView { - VStack(alignment: .leading) { - resultImage(selected.imageURL) - .frame(width: 60, height: 180) - .padding(.leading) - .padding() - Text(selected.premiereDate?.formatted(.dateTime.year().month().day()) ?? .emptyDash) - .foregroundStyle(Color.primary) - .padding() - Text(selected.overview ?? L10n.unknown) - .foregroundStyle(Color.secondary) - .padding() - Text(selected.searchProviderName ?? L10n.unknown) - .foregroundStyle(Color.secondary) - .frame(maxWidth: .infinity, alignment: .center) - .padding() - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(selected.name ?? L10n.unknown) - .navigationBarCloseButton { - selectedMatch = nil - } - .topBarTrailing { - Button(L10n.save) { - viewModel.send(.update(selected)) - selectedMatch = nil - } - .buttonStyle(.toolbarPill) - } - } - } - // MARK: - Result Image @ViewBuilder diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index 63cf5bd5c..62afb35ee 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -98,10 +98,12 @@ struct ItemEditorView: View { @ViewBuilder private var editView: some View { Section(L10n.edit) { - ChevronButton("Identify") - .onSelect { - router.route(to: \.editIdentity, viewModel.item) - } + if [.boxSet, .movie, .person, .series].contains(viewModel.item.type) { + ChevronButton("Identify") + .onSelect { + router.route(to: \.editIdentity, viewModel.item) + } + } ChevronButton(L10n.metadata) .onSelect { router.route(to: \.editMetadata, viewModel.item) From 3371d2533e739e575e441e7ba4b62dd98b642a03 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 19 Dec 2024 21:31:37 -0700 Subject: [PATCH 04/13] Rework! --- Shared/Components/ItemIdentifySearch.swift | 15 ++ .../Coordinators/ItemEditorCoordinator.swift | 13 +- Shared/Strings/Strings.swift | 4 + ...odel.swift => ItemIdentifyViewModel.swift} | 79 +++++- .../BoxSetInfoViewModel.swift | 31 --- .../MovieInfoViewModel.swift | 31 --- .../PersonInfoViewModel.swift | 31 --- .../SeriesInfoViewModel.swift | 31 --- Swiftfin.xcodeproj/project.pbxproj | 50 +--- .../Components/ItemInfoConfirmationView.swift | 4 +- .../Components/ItemInfoResultButton.swift | 2 +- .../IdentifyItemView/IdentifyItemView.swift | 248 +++--------------- .../Views/ItemEditorView/ItemEditorView.swift | 2 +- Translations/en.lproj/Localizable.strings | 6 + 14 files changed, 158 insertions(+), 389 deletions(-) create mode 100644 Shared/Components/ItemIdentifySearch.swift rename Shared/ViewModels/ItemAdministration/{ItemInfoViewModel/ItemInfoViewModel.swift => ItemIdentifyViewModel.swift} (64%) delete mode 100644 Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift delete mode 100644 Shared/ViewModels/ItemAdministration/ItemInfoViewModel/MovieInfoViewModel.swift delete mode 100644 Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift delete mode 100644 Shared/ViewModels/ItemAdministration/ItemInfoViewModel/SeriesInfoViewModel.swift diff --git a/Shared/Components/ItemIdentifySearch.swift b/Shared/Components/ItemIdentifySearch.swift new file mode 100644 index 000000000..a0d181983 --- /dev/null +++ b/Shared/Components/ItemIdentifySearch.swift @@ -0,0 +1,15 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct ItemIdentifySearch: Equatable { + var name: String? + var originalTitle: String? + var year: Int? +} diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 312302b42..9c11df5f4 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -64,18 +64,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @ViewBuilder func makeEditIdentity(item: BaseItemDto) -> some View { - switch item.type { - case .boxSet: - IdentifyItemView(viewModel: BoxSetInfoViewModel(item: item)) - case .movie: - IdentifyItemView(viewModel: MovieInfoViewModel(item: item)) - case .person: - IdentifyItemView(viewModel: PersonInfoViewModel(item: item)) - case .series: - IdentifyItemView(viewModel: SeriesInfoViewModel(item: item)) - default: - ErrorView(error: JellyfinAPIError("Invalid media type")) - } + ItemIdentifyView(item: item) } func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator { diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 472beedb9..52525d91b 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -680,6 +680,8 @@ internal enum L10n { internal static let home = L10n.tr("Localizable", "home", fallback: "Home") /// Hours internal static let hours = L10n.tr("Localizable", "hours", fallback: "Hours") + /// Identify + internal static let identify = L10n.tr("Localizable", "identify", fallback: "Identify") /// Idle internal static let idle = L10n.tr("Localizable", "idle", fallback: "Idle") /// Illustrator @@ -1512,6 +1514,8 @@ internal enum L10n { internal static let unsavedChangesMessage = L10n.tr("Localizable", "unsavedChangesMessage", fallback: "You have unsaved changes. Are you sure you want to discard them?") /// URL internal static let url = L10n.tr("Localizable", "url", fallback: "URL") + /// Use as item + internal static let useAsItem = L10n.tr("Localizable", "useAsItem", fallback: "Use as item") /// Use as Transcoding Profile internal static let useAsTranscodingProfile = L10n.tr("Localizable", "useAsTranscodingProfile", fallback: "Use as Transcoding Profile") /// Use Primary Image diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift similarity index 64% rename from Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift rename to Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift index 0901a5a31..8af013b3b 100644 --- a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/ItemInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift @@ -12,7 +12,7 @@ import Get import JellyfinAPI import OrderedCollections -class ItemInfoViewModel: ViewModel, Stateful, Eventful { +class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { // MARK: - Events @@ -26,7 +26,7 @@ class ItemInfoViewModel: ViewModel, Stateful, Eventful { enum Action: Equatable { case cancel - case search(SearchInfo) + case search(ItemIdentifySearch) case update(RemoteSearchResult) } @@ -83,7 +83,7 @@ class ItemInfoViewModel: ViewModel, Stateful, Eventful { return state - case let .search(searchInfo): + case let .search(itemIdentifySearch): searchTask?.cancel() searchTask = Task { [weak self] in @@ -94,7 +94,7 @@ class ItemInfoViewModel: ViewModel, Stateful, Eventful { _ = self.backgroundStates.append(.searching) } - let allElements = try await self.searchItem(searchInfo) + let allElements = try await self.searchItem(itemIdentifySearch) await MainActor.run { self.searchResults = allElements @@ -142,8 +142,75 @@ class ItemInfoViewModel: ViewModel, Stateful, Eventful { // MARK: - Return Matching Elements (To Be Overridden) - func searchItem(_ searchInfo: SearchInfo) async throws -> [RemoteSearchResult] { - fatalError("This method should be overridden in subclasses") + private func searchItem(_ itemIdentifySearch: ItemIdentifySearch) async throws -> [RemoteSearchResult] { + guard let itemId = item.id, let itemType = item.type else { + return [] + } + + let name = itemIdentifySearch.name + let originalTitle = itemIdentifySearch.originalTitle + let year = itemIdentifySearch.year + + switch itemType { + case .boxSet: + let parameters = BoxSetInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: BoxSetInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getBoxSetRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + case .movie: + let parameters = MovieInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: MovieInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getMovieRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + case .person: + let parameters = PersonLookupInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: PersonLookupInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getPersonRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + case .series: + let parameters = SeriesInfoRemoteSearchQuery( + itemID: itemId, + searchInfo: SeriesInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getSeriesRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + default: + return [] + } } // MARK: - Save Updated Item to Server diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift deleted file mode 100644 index 018eec8d7..000000000 --- a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/BoxSetInfoViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI - -class BoxSetInfoViewModel: ItemInfoViewModel { - - // MARK: - Return Matching Box Set - - override func searchItem(_ boxSetInfo: BoxSetInfo) async throws -> [RemoteSearchResult] { - guard let itemId = item.id, item.type == .boxSet else { - return [] - } - - let parameters = BoxSetInfoRemoteSearchQuery( - itemID: itemId, - searchInfo: boxSetInfo - ) - let request = Paths.getBoxSetRemoteSearchResults(parameters) - let response = try await userSession.client.send(request) - - return response.value - } -} diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/MovieInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/MovieInfoViewModel.swift deleted file mode 100644 index 21637e9f8..000000000 --- a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/MovieInfoViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI - -class MovieInfoViewModel: ItemInfoViewModel { - - // MARK: - Return Matching Movies - - override func searchItem(_ movieInfo: MovieInfo) async throws -> [RemoteSearchResult] { - guard let itemId = item.id, item.type == .movie else { - return [] - } - - let parameters = MovieInfoRemoteSearchQuery( - itemID: itemId, - searchInfo: movieInfo - ) - let request = Paths.getMovieRemoteSearchResults(parameters) - let response = try await userSession.client.send(request) - - return response.value - } -} diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift deleted file mode 100644 index 4bd76d8fd..000000000 --- a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/PersonInfoViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI - -class PersonInfoViewModel: ItemInfoViewModel { - - // MARK: - Return Matching People - - override func searchItem(_ personLookupInfo: PersonLookupInfo) async throws -> [RemoteSearchResult] { - guard let itemId = item.id, item.type == .person else { - return [] - } - - let parameters = PersonLookupInfoRemoteSearchQuery( - itemID: itemId, - searchInfo: personLookupInfo - ) - let request = Paths.getPersonRemoteSearchResults(parameters) - let response = try await userSession.client.send(request) - - return response.value - } -} diff --git a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/SeriesInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/SeriesInfoViewModel.swift deleted file mode 100644 index 79910e4e7..000000000 --- a/Shared/ViewModels/ItemAdministration/ItemInfoViewModel/SeriesInfoViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI - -class SeriesInfoViewModel: ItemInfoViewModel { - - // MARK: - Return Matching Series - - override func searchItem(_ seriesInfo: SeriesInfo) async throws -> [RemoteSearchResult] { - guard let itemId = item.id, item.type == .series else { - return [] - } - - let parameters = SeriesInfoRemoteSearchQuery( - itemID: itemId, - searchInfo: seriesInfo - ) - let request = Paths.getSeriesRemoteSearchResults(parameters) - let response = try await userSession.client.send(request) - - return response.value - } -} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index cfc3e65a5..8c56414f7 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -99,6 +99,8 @@ 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; + 4E63190C2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */; }; + 4E63190D2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */; }; 4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; }; 4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; }; 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; }; @@ -209,20 +211,12 @@ 4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; }; 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */; }; - 4EE766F72D132054009658F0 /* ItemInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */; }; - 4EE766F82D132054009658F0 /* ItemInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */; }; + 4EE766F72D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */; }; + 4EE766F82D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */; }; 4EE766FA2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; - 4EE766FE2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */; }; - 4EE766FF2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */; }; - 4EE767012D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */; }; - 4EE767022D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */; }; 4EE767082D13403F009658F0 /* ItemInfoResultButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */; }; 4EE7670A2D135CBA009658F0 /* ItemInfoConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */; }; - 4EE7670C2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */; }; - 4EE7670D2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */; }; - 4EE7670F2D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */; }; - 4EE767102D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */; }; 4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; }; 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; }; 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; }; @@ -1252,6 +1246,7 @@ 4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserLiveTVAccessView.swift; sourceTree = ""; }; 4E556AAF2D036F5E00733377 /* UserPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissions.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; + 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIdentifySearch.swift; sourceTree = ""; }; 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = ""; }; 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = ""; }; 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentalRating.swift; sourceTree = ""; }; @@ -1341,14 +1336,10 @@ 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = ""; }; 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemView.swift; sourceTree = ""; }; - 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoViewModel.swift; sourceTree = ""; }; + 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIdentifyViewModel.swift; sourceTree = ""; }; 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResult.swift; sourceTree = ""; }; - 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfoViewModel.swift; sourceTree = ""; }; - 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesInfoViewModel.swift; sourceTree = ""; }; 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoResultButton.swift; sourceTree = ""; }; 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoConfirmationView.swift; sourceTree = ""; }; - 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonInfoViewModel.swift; sourceTree = ""; }; - 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxSetInfoViewModel.swift; sourceTree = ""; }; 4EED87462CBF824B002354D2 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = ""; }; 4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; }; @@ -2488,8 +2479,8 @@ isa = PBXGroup; children = ( 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, - 4EE766FC2D132CDB009658F0 /* ItemInfoViewModel */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, + 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, ); path = ItemAdministration; @@ -2723,18 +2714,6 @@ path = IdentifyItemView; sourceTree = ""; }; - 4EE766FC2D132CDB009658F0 /* ItemInfoViewModel */ = { - isa = PBXGroup; - children = ( - 4EE7670E2D136B4A009658F0 /* BoxSetInfoViewModel.swift */, - 4EE766F62D132043009658F0 /* ItemInfoViewModel.swift */, - 4EE766FD2D132DE5009658F0 /* MovieInfoViewModel.swift */, - 4EE7670B2D136A75009658F0 /* PersonInfoViewModel.swift */, - 4EE767002D132F48009658F0 /* SeriesInfoViewModel.swift */, - ); - path = ItemInfoViewModel; - sourceTree = ""; - }; 4EE767062D13401C009658F0 /* Components */ = { isa = PBXGroup; children = ( @@ -4394,6 +4373,7 @@ E1A1528728FD229500600579 /* ChevronButton.swift */, E1153DCB2BBB633B00424D36 /* FastSVGView.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, + 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */, 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */, 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */, E1D37F472B9C648E00343D2B /* MaxHeightText.swift */, @@ -5089,6 +5069,7 @@ E1B490452967E26300D3EDCE /* PersistentLogHandler.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, E18E021E2887492B0022598C /* RowDivider.swift in Sources */, + 4E63190D2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */, 4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */, 4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */, E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */, @@ -5153,7 +5134,6 @@ E1E9EFEB28C7EA2C00CC1F8B /* UserDto.swift in Sources */, E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */, 4E2AC4C32C6C491200DD600D /* AudoCodec.swift in Sources */, - 4EE766FF2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */, E1575EA6293E7D40001665B1 /* VideoPlayer.swift in Sources */, E185920628CDAA6400326F80 /* CastAndCrewHStack.swift in Sources */, E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */, @@ -5166,7 +5146,6 @@ E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */, E1575E85293E7A00001665B1 /* DarkAppIcon.swift in Sources */, 4E49DED22CE54D6D00352DCD /* ActiveSessionsPolicy.swift in Sources */, - 4EE767102D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */, E1763A762BF3FF01004DF6AB /* AppLoadingView.swift in Sources */, E18121062CBE428000682985 /* ChevronButton.swift in Sources */, E102315A2BCF8AF8009D71FC /* ProgramButtonContent.swift in Sources */, @@ -5193,7 +5172,7 @@ E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */, E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */, E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */, - 4EE766F82D132054009658F0 /* ItemInfoViewModel.swift in Sources */, + 4EE766F82D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */, E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */, E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */, BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */, @@ -5317,7 +5296,6 @@ E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, E1DABAFC2A270EE7008AC34A /* MediaSourcesCard.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, - 4EE767022D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */, E14EDEC62B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */, E14EDEC92B8FB65F000F00A4 /* ItemFilterType.swift in Sources */, E1D37F4C2B9CEA5C00343D2B /* ImageSource.swift in Sources */, @@ -5334,7 +5312,6 @@ E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */, E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */, E1ED7FE22CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, - 4EE7670D2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */, E1CB75762C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E102315B2BCF8AF8009D71FC /* ProgramProgressOverlay.swift in Sources */, E1E6C45129B104850064123F /* Button.swift in Sources */, @@ -5604,7 +5581,6 @@ E18E01FA288747580022598C /* AboutAppView.swift in Sources */, E170D103294CE8BF0017224C /* LoadingView.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, - 4EE7670C2D136A7E009658F0 /* PersonInfoViewModel.swift in Sources */, E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */, E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */, E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, @@ -5847,7 +5823,6 @@ 4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */, 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */, E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */, - 4EE7670F2D136B4A009658F0 /* BoxSetInfoViewModel.swift in Sources */, E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */, E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */, E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */, @@ -5899,7 +5874,6 @@ 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */, E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, - 4EE767012D132F48009658F0 /* SeriesInfoViewModel.swift in Sources */, E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */, 4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */, 4E537A842D03D11200659A1A /* ServerUserDeviceAccessView.swift in Sources */, @@ -6007,7 +5981,6 @@ 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */, E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */, - 4EE766FE2D132DEF009658F0 /* MovieInfoViewModel.swift in Sources */, 4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */, E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */, E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */, @@ -6027,7 +6000,7 @@ E1AD105F26D9ADDD003E4A08 /* NameGuidPair.swift in Sources */, 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */, E18A8E7D28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, - 4EE766F72D132054009658F0 /* ItemInfoViewModel.swift in Sources */, + 4EE766F72D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */, 4EC2B19B2CC96E7400D866BE /* ServerUsersView.swift in Sources */, E18E01F1288747230022598C /* PlayButton.swift in Sources */, E129429028F0BDC300796AC6 /* TimeStampType.swift in Sources */, @@ -6057,6 +6030,7 @@ BD3957752C112A330078CEF8 /* ButtonSection.swift in Sources */, E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */, + 4E63190C2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */, E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */, 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */, E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift index a198cda16..caf593cad 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift @@ -9,7 +9,7 @@ import JellyfinAPI import SwiftUI -extension IdentifyItemView { +extension ItemIdentifyView { struct ItemInfoConfirmationView: View { @@ -51,7 +51,7 @@ extension IdentifyItemView { } .padding() .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Item") + .navigationTitle(L10n.useAsItem.localizedCapitalized) .navigationBarCloseButton { onClose() } diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift index 363126217..7c6289cec 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift @@ -10,7 +10,7 @@ import Combine import JellyfinAPI import SwiftUI -extension IdentifyItemView { +extension ItemIdentifyView { struct RemoteSearchResultButton: View { diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift index c1493c8d2..549d5cbfa 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -10,7 +10,7 @@ import Combine import JellyfinAPI import SwiftUI -struct IdentifyItemView: View { +struct ItemIdentifyView: View { // MARK: - Observed & Environment Objects @@ -18,14 +18,10 @@ struct IdentifyItemView: View { private var router: ItemEditorCoordinator.Router @ObservedObject - private var viewModel: ItemInfoViewModel + private var viewModel: ItemIdentifyViewModel // MARK: - Identity Variables - @State - private var lastSearch: SearchInfo? - @State - private var tempSearchInfo: SearchInfo? @State private var selectedMatch: RemoteSearchResult? @@ -34,21 +30,17 @@ struct IdentifyItemView: View { @State private var error: Error? - // MARK: - Potential States + // MARK: - Lookup States @State - var boxSetInfo = BoxSetInfo() - @State - var movieInfo = MovieInfo() + var search = ItemIdentifySearch() @State - var personInfo = PersonLookupInfo() - @State - var seriesInfo = SeriesInfo() + var lastSearch = ItemIdentifySearch() // MARK: - Initializer - init(viewModel: ItemInfoViewModel) { - self.viewModel = viewModel + init(item: BaseItemDto) { + self.viewModel = .init(item: item) } // MARK: - Body @@ -63,7 +55,7 @@ struct IdentifyItemView: View { contentView } } - .navigationBarTitle(L10n.metadata) + .navigationBarTitle(L10n.identify) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: Binding( get: { selectedMatch != nil }, @@ -81,16 +73,6 @@ struct IdentifyItemView: View { } } } - .topBarTrailing { - Button(L10n.search) { - if let tempSearchInfo { - lastSearch = tempSearchInfo - viewModel.send(.search(tempSearchInfo)) - } - } - .buttonStyle(.toolbarPill) - .disabled(viewModel.state == .updating || tempSearchInfo == nil || lastSearch == tempSearchInfo) - } .navigationBarBackButtonHidden(viewModel.state == .updating) .onReceive(viewModel.events) { events in switch events { @@ -109,7 +91,7 @@ struct IdentifyItemView: View { @ViewBuilder var updateView: some View { - VStack(alignment: .center) { + VStack(alignment: .center, spacing: 16) { ProgressView() Button(L10n.cancel, role: .destructive) { viewModel.send(.cancel) @@ -124,26 +106,46 @@ struct IdentifyItemView: View { @ViewBuilder var contentView: some View { Form { - switch viewModel.item.type { - case .boxSet: - boxSetSearchView - case .movie: - movieSearchView - case .person: - personSearchView - case .series: - seriesSearchView - default: - EmptyView() + searchView + resultsView + } + .topBarTrailing { + Button(L10n.search) { + viewModel.send(.search(search)) + lastSearch = search } - searchResultsView + .buttonStyle(.toolbarPill) + .disabled(viewModel.state == .updating || search == lastSearch) } } - // MARK: - Search Results + // MARK: - Search View @ViewBuilder - private var searchResultsView: some View { + private var searchView: some View { + Section(header: Text(L10n.search)) { + TextField(L10n.title, text: Binding( + get: { search.name ?? "" }, + set: { search.name = $0.isEmpty ? nil : $0 } + )) + + TextField(L10n.originalTitle, text: Binding( + get: { search.originalTitle ?? "" }, + set: { search.originalTitle = $0.isEmpty ? nil : $0 } + )) + + TextField(L10n.year, text: Binding( + get: { search.year?.description ?? "" }, + set: { search.year = $0.isEmpty ? nil : Int($0) } + )) + .keyboardType(.numberPad) + } + } + + // MARK: - Results View + + @ViewBuilder + private var resultsView: some View { if viewModel.searchResults.isNotEmpty { Section(L10n.items) { ForEach(viewModel.searchResults, id: \.id) { remoteSearchResult in @@ -158,170 +160,6 @@ struct IdentifyItemView: View { } } - // MARK: - Box Set Search View - - @ViewBuilder - private var boxSetSearchView: some View { - Section(header: Text(L10n.search)) { - TextField( - L10n.name, - text: Binding( - get: { boxSetInfo.name ?? "" }, - set: { newValue in - boxSetInfo.name = newValue - tempSearchInfo = boxSetInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.originalTitle, - text: Binding( - get: { boxSetInfo.originalTitle ?? "" }, - set: { newValue in - boxSetInfo.originalTitle = newValue - tempSearchInfo = boxSetInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.year, - text: Binding( - get: { boxSetInfo.year.map(String.init) ?? "" }, - set: { newValue in - boxSetInfo.year = Int(newValue) - tempSearchInfo = boxSetInfo as? SearchInfo - } - ) - ) - .keyboardType(.numberPad) - } - } - - // MARK: - Movie Search View - - @ViewBuilder - private var movieSearchView: some View { - Section(header: Text(L10n.search)) { - TextField( - L10n.name, - text: Binding( - get: { movieInfo.name ?? "" }, - set: { newValue in - movieInfo.name = newValue - tempSearchInfo = movieInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.originalTitle, - text: Binding( - get: { movieInfo.originalTitle ?? "" }, - set: { newValue in - movieInfo.originalTitle = newValue - tempSearchInfo = movieInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.year, - text: Binding( - get: { movieInfo.year.map(String.init) ?? "" }, - set: { newValue in - movieInfo.year = Int(newValue) - tempSearchInfo = movieInfo as? SearchInfo - } - ) - ) - .keyboardType(.numberPad) - } - } - - // MARK: - Person Search View - - @ViewBuilder - private var personSearchView: some View { - Section(header: Text(L10n.search)) { - TextField( - L10n.name, - text: Binding( - get: { personInfo.name ?? "" }, - set: { newValue in - personInfo.name = newValue - tempSearchInfo = personInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.originalTitle, - text: Binding( - get: { personInfo.originalTitle ?? "" }, - set: { newValue in - personInfo.originalTitle = newValue - tempSearchInfo = personInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.year, - text: Binding( - get: { personInfo.year.map(String.init) ?? "" }, - set: { newValue in - personInfo.year = Int(newValue) - tempSearchInfo = personInfo as? SearchInfo - } - ) - ) - .keyboardType(.numberPad) - } - } - - // MARK: - Series Search View - - @ViewBuilder - private var seriesSearchView: some View { - Section(header: Text(L10n.search)) { - TextField( - L10n.name, - text: Binding( - get: { seriesInfo.name ?? "" }, - set: { newValue in - seriesInfo.name = newValue - tempSearchInfo = seriesInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.originalTitle, - text: Binding( - get: { seriesInfo.originalTitle ?? "" }, - set: { newValue in - seriesInfo.originalTitle = newValue - tempSearchInfo = seriesInfo as? SearchInfo - } - ) - ) - - TextField( - L10n.year, - text: Binding( - get: { seriesInfo.year.map(String.init) ?? "" }, - set: { newValue in - seriesInfo.year = Int(newValue) - tempSearchInfo = seriesInfo as? SearchInfo - } - ) - ) - .keyboardType(.numberPad) - } - } - // MARK: - Result Image @ViewBuilder diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index 62afb35ee..19b8d8f3f 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -99,7 +99,7 @@ struct ItemEditorView: View { private var editView: some View { Section(L10n.edit) { if [.boxSet, .movie, .person, .series].contains(viewModel.item.type) { - ChevronButton("Identify") + ChevronButton(L10n.identify) .onSelect { router.route(to: \.editIdentity, viewModel.item) } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 84ddeb8d9..75d5f2cc7 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -2300,3 +2300,9 @@ // Delete Selected Schedules - Button // Button label for deleting all selected Access Schedules "deleteSelectedSchedules" = "Delete Selected Schedules"; + +/// Identify +"identify" = "Identify"; + +/// Use as item +"useAsItem" = "Use as item"; From c1b75d9d3ea6164ced1e90b7da89a1a988a80f44 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 29 Dec 2024 02:57:48 -0700 Subject: [PATCH 05/13] Organization, new LoadingIcon, remove unnecessary components, and standardize: CancellableLoadingButton --- .../Components/CancellableLoadingButton.swift | 48 +++++++++++++++++++ Shared/Components/ItemIdentifySearch.swift | 15 ------ Shared/Strings/Strings.swift | 2 + .../ItemIdentifyViewModel.swift | 21 ++++---- .../VideoPlayer/Components/LoadingView.swift | 21 ++------ Swiftfin.xcodeproj/project.pbxproj | 30 +++++++----- .../AddItemElementView.swift | 0 .../Components/NameInput.swift | 0 .../Components/SearchResultsSection.swift | 0 .../Components/EditItemElementRow.swift | 0 .../EditItemElementView.swift | 0 .../Components/ItemInfoConfirmationView.swift | 0 .../Components/ItemInfoResultButton.swift | 0 .../IdentifyItemView.swift | 30 +++++++----- .../VideoPlayer/Components/LoadingView.swift | 21 ++------ Translations/en.lproj/Localizable.strings | 5 +- 16 files changed, 111 insertions(+), 82 deletions(-) create mode 100644 Shared/Components/CancellableLoadingButton.swift delete mode 100644 Shared/Components/ItemIdentifySearch.swift rename Swiftfin/Views/ItemEditorView/{ => ItemElements}/AddItemElementView/AddItemElementView.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemElements}/AddItemElementView/Components/NameInput.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemElements}/AddItemElementView/Components/SearchResultsSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemElements}/EditItemElementView/Components/EditItemElementRow.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemElements}/EditItemElementView/EditItemElementView.swift (100%) rename Swiftfin/Views/ItemEditorView/{IdentifyItemView => ItemIdentification}/Components/ItemInfoConfirmationView.swift (100%) rename Swiftfin/Views/ItemEditorView/{IdentifyItemView => ItemIdentification}/Components/ItemInfoResultButton.swift (100%) rename Swiftfin/Views/ItemEditorView/{IdentifyItemView => ItemIdentification}/IdentifyItemView.swift (87%) diff --git a/Shared/Components/CancellableLoadingButton.swift b/Shared/Components/CancellableLoadingButton.swift new file mode 100644 index 000000000..326fd4631 --- /dev/null +++ b/Shared/Components/CancellableLoadingButton.swift @@ -0,0 +1,48 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct CancellableLoadingButton: View { + + // MARK: - Button Variables + + private let description: String? + private let onCancel: () -> Void + + // MARK: - Initializer + + init(_ description: String?, onCancel: @escaping () -> Void) { + self.description = description + self.onCancel = onCancel + } + + // MARK: - Body + + var body: some View { + VStack(spacing: 10) { + if let description { + Text(description) + .foregroundColor(.primary) + } + + ProgressView() + .padding() + + Button(role: .cancel, action: onCancel) { + Text(L10n.cancel) + .foregroundColor(.red) + .padding() + .overlay { + Capsule() + .stroke(Color.red, lineWidth: 1) + } + } + } + } +} diff --git a/Shared/Components/ItemIdentifySearch.swift b/Shared/Components/ItemIdentifySearch.swift deleted file mode 100644 index a0d181983..000000000 --- a/Shared/Components/ItemIdentifySearch.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct ItemIdentifySearch: Equatable { - var name: String? - var originalTitle: String? - var year: Int? -} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 83f5208c0..0ccb64e40 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -106,6 +106,8 @@ internal enum L10n { internal static let appIcon = L10n.tr("Localizable", "appIcon", fallback: "App Icon") /// Application Name internal static let applicationName = L10n.tr("Localizable", "applicationName", fallback: "Application Name") + /// Applying media information + internal static let applyingMediaInformation = L10n.tr("Localizable", "applyingMediaInformation", fallback: "Applying media information") /// Arranger internal static let arranger = L10n.tr("Localizable", "arranger", fallback: "Arranger") /// Artist diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift index 8af013b3b..83a0edf07 100644 --- a/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift @@ -26,7 +26,7 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { enum Action: Equatable { case cancel - case search(ItemIdentifySearch) + case search(name: String? = nil, originalTitle: String? = nil, year: Int? = nil) case update(RemoteSearchResult) } @@ -83,7 +83,7 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { return state - case let .search(itemIdentifySearch): + case let .search(name, originalTitle, year): searchTask?.cancel() searchTask = Task { [weak self] in @@ -94,7 +94,11 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { _ = self.backgroundStates.append(.searching) } - let allElements = try await self.searchItem(itemIdentifySearch) + let allElements = try await self.searchItem( + name: name, + originalTitle: originalTitle, + year: year + ) await MainActor.run { self.searchResults = allElements @@ -142,15 +146,16 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { // MARK: - Return Matching Elements (To Be Overridden) - private func searchItem(_ itemIdentifySearch: ItemIdentifySearch) async throws -> [RemoteSearchResult] { + private func searchItem( + name: String?, + originalTitle: String?, + year: Int? + ) async throws -> [RemoteSearchResult] { + guard let itemId = item.id, let itemType = item.type else { return [] } - let name = itemIdentifySearch.name - let originalTitle = itemIdentifySearch.originalTitle - let year = itemIdentifySearch.year - switch itemType { case .boxSet: let parameters = BoxSetInfoRemoteSearchQuery( diff --git a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift index 90cbfb6be..d8112cbf4 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift @@ -20,25 +20,10 @@ extension VideoPlayer { ZStack { Color.black - VStack(spacing: 10) { - - Text(L10n.retrievingMediaInformation) - .foregroundColor(.white) - - ProgressView() - - Button { - router.dismissCoordinator() - } label: { - Text(L10n.cancel) - .foregroundColor(.red) - .padding() - .overlay { - Capsule() - .stroke(Color.red, lineWidth: 1) - } - } + CancellableLoadingButton(L10n.retrievingMediaInformation) { + router.dismissCoordinator() } + .foregroundStyle(.white) } } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 55ab61de0..bf211d416 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -67,6 +67,8 @@ 4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; }; + 4E37661B2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */; }; + 4E37661C2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */; }; 4E3A24DA2CFE34A00083A72C /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */; }; 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; @@ -102,8 +104,6 @@ 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; - 4E63190C2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */; }; - 4E63190D2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */; }; 4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; }; 4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; }; 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; }; @@ -1230,6 +1230,7 @@ 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = ""; }; 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = ""; }; 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = ""; }; + 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableLoadingButton.swift; sourceTree = ""; }; 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsSection.swift; sourceTree = ""; }; 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInput.swift; sourceTree = ""; }; 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = ""; }; @@ -1255,7 +1256,6 @@ 4E5508722D13AFE3002A5345 /* UserProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImage.swift; sourceTree = ""; }; 4E556AAF2D036F5E00733377 /* UserPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissions.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; - 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIdentifySearch.swift; sourceTree = ""; }; 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = ""; }; 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = ""; }; 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentalRating.swift; sourceTree = ""; }; @@ -2260,6 +2260,15 @@ path = ServerLogsView; sourceTree = ""; }; + 4E3766192D2144BA00C5D7A5 /* ItemElements */ = { + isa = PBXGroup; + children = ( + 4E5071E22CFCEFC3003FA2AD /* AddItemElementView */, + 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, + ); + path = ItemElements; + sourceTree = ""; + }; 4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */ = { isa = PBXGroup; children = ( @@ -2504,11 +2513,10 @@ 4E8F74A32CE03D3100CC8969 /* ItemEditorView */ = { isa = PBXGroup; children = ( - 4E5071E22CFCEFC3003FA2AD /* AddItemElementView */, 4E8F74A62CE03D4C00CC8969 /* Components */, - 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, 4E6619FF2CEFE39000025C99 /* EditMetadataView */, - 4EE766F32D131F6E009658F0 /* IdentifyItemView */, + 4E3766192D2144BA00C5D7A5 /* ItemElements */, + 4EE766F32D131F6E009658F0 /* ItemIdentification */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, ); path = ItemEditorView; @@ -2785,13 +2793,13 @@ path = EditAccessScheduleView; sourceTree = ""; }; - 4EE766F32D131F6E009658F0 /* IdentifyItemView */ = { + 4EE766F32D131F6E009658F0 /* ItemIdentification */ = { isa = PBXGroup; children = ( 4EE767062D13401C009658F0 /* Components */, 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */, ); - path = IdentifyItemView; + path = ItemIdentification; sourceTree = ""; }; 4EE767062D13401C009658F0 /* Components */ = { @@ -4450,11 +4458,11 @@ E104DC952B9E7E29008F506D /* AssertionFailureView.swift */, E18E0203288749200022598C /* BlurView.swift */, E145EB212BDCCA43003BF6F3 /* BulletedList.swift */, + 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */, 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */, E1A1528728FD229500600579 /* ChevronButton.swift */, E1153DCB2BBB633B00424D36 /* FastSVGView.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, - 4E63190B2D15220B00CB92FA /* ItemIdentifySearch.swift */, 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */, 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */, E1D37F472B9C648E00343D2B /* MaxHeightText.swift */, @@ -5193,7 +5201,6 @@ E1B490452967E26300D3EDCE /* PersistentLogHandler.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, E18E021E2887492B0022598C /* RowDivider.swift in Sources */, - 4E63190D2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */, 4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */, 4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */, E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */, @@ -5351,6 +5358,7 @@ 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, E10B1EC22BD9AD6100A92EAF /* V1UserModel.swift in Sources */, E1E2F8432B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */, + 4E37661B2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */, E111D8F628D03B7500400001 /* PagingLibraryViewModel.swift in Sources */, E1575E87293E7A00001665B1 /* InvertedDarkAppIcon.swift in Sources */, E10432F72BE4426F006FF9DD /* FormatStyle.swift in Sources */, @@ -5795,6 +5803,7 @@ E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */, 4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */, 4E2AC4CB2C6C494E00DD600D /* VideoCodec.swift in Sources */, + 4E37661C2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */, E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */, 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */, E18E0204288749200022598C /* RowDivider.swift in Sources */, @@ -6160,7 +6169,6 @@ BD3957752C112A330078CEF8 /* ButtonSection.swift in Sources */, E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */, - 4E63190C2D15220F00CB92FA /* ItemIdentifySearch.swift in Sources */, E13DD3F92717E961009D4DAF /* SelectUserViewModel.swift in Sources */, 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */, E1194F502BEB1E3000888DB6 /* StoredValues+Temp.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/NameInput.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/Components/NameInput.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/SearchResultsSection.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/Components/SearchResultsSection.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoConfirmationView.swift rename to Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/ItemInfoResultButton.swift rename to Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/IdentifyItemView.swift similarity index 87% rename from Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift rename to Swiftfin/Views/ItemEditorView/ItemIdentification/IdentifyItemView.swift index 549d5cbfa..2e34539f7 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemIdentification/IdentifyItemView.swift @@ -30,12 +30,20 @@ struct ItemIdentifyView: View { @State private var error: Error? + // MARK: - Lookup Truple + + private struct searchComponents: Equatable { + var name: String? + var originalTitle: String? + var year: Int? + } + // MARK: - Lookup States @State - var search = ItemIdentifySearch() + private var search = searchComponents() @State - var lastSearch = ItemIdentifySearch() + private var lastSearch = searchComponents() // MARK: - Initializer @@ -87,18 +95,14 @@ struct ItemIdentifyView: View { .errorMessage($error) } - // MARK: - UpdatE View + // MARK: - Update View @ViewBuilder var updateView: some View { - VStack(alignment: .center, spacing: 16) { - ProgressView() - Button(L10n.cancel, role: .destructive) { - viewModel.send(.cancel) - } - .buttonStyle(.borderedProminent) - .tint(.red) + CancellableLoadingButton(L10n.applyingMediaInformation) { + viewModel.send(.cancel) } + .foregroundStyle(.primary) } // MARK: - Content View @@ -111,7 +115,11 @@ struct ItemIdentifyView: View { } .topBarTrailing { Button(L10n.search) { - viewModel.send(.search(search)) + viewModel.send(.search( + name: search.name, + originalTitle: search.originalTitle, + year: search.year + )) lastSearch = search } .buttonStyle(.toolbarPill) diff --git a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift index 90cbfb6be..d8112cbf4 100644 --- a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift @@ -20,25 +20,10 @@ extension VideoPlayer { ZStack { Color.black - VStack(spacing: 10) { - - Text(L10n.retrievingMediaInformation) - .foregroundColor(.white) - - ProgressView() - - Button { - router.dismissCoordinator() - } label: { - Text(L10n.cancel) - .foregroundColor(.red) - .padding() - .overlay { - Capsule() - .stroke(Color.red, lineWidth: 1) - } - } + CancellableLoadingButton(L10n.retrievingMediaInformation) { + router.dismissCoordinator() } + .foregroundStyle(.white) } } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index fa94e6780..5c23bbfc1 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -136,6 +136,9 @@ /// Application Name "applicationName" = "Application Name"; +/// Applying media information +"applyingMediaInformation" = "Applying media information"; + /// Arranger "arranger" = "Arranger"; @@ -1964,4 +1967,4 @@ "yellow" = "Yellow"; /// Yes -"yes" = "Yes"; +"yes" = "Yes"; \ No newline at end of file From 283c7e9569a3564022b537afd7802001eb824f16 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 29 Dec 2024 15:49:45 -0700 Subject: [PATCH 06/13] Organization & Static Method Re-Use. --- .../Coordinators/ItemEditorCoordinator.swift | 2 +- Swiftfin.xcodeproj/project.pbxproj | 8 ++--- .../Components/ItemInfoConfirmationView.swift | 7 ++--- .../Components/ItemInfoResultButton.swift | 30 ++++++++----------- ...iew.swift => ItemIdentificationView.swift} | 18 +++++------ 5 files changed, 29 insertions(+), 36 deletions(-) rename Swiftfin/Views/ItemEditorView/ItemIdentification/{IdentifyItemView.swift => ItemIdentificationView.swift} (90%) diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 9c11df5f4..de33776cc 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -64,7 +64,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @ViewBuilder func makeEditIdentity(item: BaseItemDto) -> some View { - ItemIdentifyView(item: item) + ItemIdentificationView(item: item) } func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator { diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index bf211d416..62ab4117a 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -218,7 +218,7 @@ 4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; }; - 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */; }; + 4EE766F52D131FBC009658F0 /* ItemIdentificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F42D131FB7009658F0 /* ItemIdentificationView.swift */; }; 4EE766F72D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */; }; 4EE766F82D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */; }; 4EE766FA2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; @@ -1349,7 +1349,7 @@ 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = ""; }; - 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemView.swift; sourceTree = ""; }; + 4EE766F42D131FB7009658F0 /* ItemIdentificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIdentificationView.swift; sourceTree = ""; }; 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIdentifyViewModel.swift; sourceTree = ""; }; 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResult.swift; sourceTree = ""; }; 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoResultButton.swift; sourceTree = ""; }; @@ -2797,7 +2797,7 @@ isa = PBXGroup; children = ( 4EE767062D13401C009658F0 /* Components */, - 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */, + 4EE766F42D131FB7009658F0 /* ItemIdentificationView.swift */, ); path = ItemIdentification; sourceTree = ""; @@ -5724,7 +5724,7 @@ 4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */, 4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */, 4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */, - 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */, + 4EE766F52D131FBC009658F0 /* ItemIdentificationView.swift in Sources */, 4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */, 4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */, 4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift index caf593cad..f353c0869 100644 --- a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift @@ -9,14 +9,13 @@ import JellyfinAPI import SwiftUI -extension ItemIdentifyView { +extension ItemIdentificationView { struct ItemInfoConfirmationView: View { // MARK: - Item Info Variables let itemInfo: RemoteSearchResult - let remoteImage: any View // MARK: - Item Info Actions @@ -28,9 +27,7 @@ extension ItemIdentifyView { var body: some View { NavigationView { VStack(alignment: .leading, spacing: 16) { - remoteImage - .eraseToAnyView() - .frame(width: 60, height: 180, alignment: .leading) + ItemIdentificationView.resultImage(itemInfo.imageURL) Text(itemInfo.name ?? L10n.unknown) .foregroundStyle(Color.primary) diff --git a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift index 7c6289cec..8a486dfcf 100644 --- a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift +++ b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift @@ -10,39 +10,24 @@ import Combine import JellyfinAPI import SwiftUI -extension ItemIdentifyView { +extension ItemIdentificationView { struct RemoteSearchResultButton: View { // MARK: - Remote Search Result Variable let remoteSearchResult: RemoteSearchResult - let remoteImage: any View // MARK: - Remote Search Result Action let onSelect: () -> Void - // MARK: - Result Title - - private var resultTitle: String { - let name = remoteSearchResult.name ?? L10n.unknown - let year = remoteSearchResult.productionYear?.description ?? - remoteSearchResult.premiereDate?.formatted(.dateTime.year()).description ?? - .emptyDash - - return "\(name) (\(year))" - } - // MARK: - Body var body: some View { Button(action: onSelect) { HStack { - remoteImage - .eraseToAnyView() - .frame(width: 30, height: 90) - .padding(.horizontal) + ItemIdentificationView.resultImage(remoteSearchResult.imageURL) VStack(alignment: .leading) { Text( @@ -61,5 +46,16 @@ extension ItemIdentifyView { } } } + + // MARK: - Result Title + + private var resultTitle: String { + let name = remoteSearchResult.name ?? L10n.unknown + let year = remoteSearchResult.productionYear?.description ?? + remoteSearchResult.premiereDate?.formatted(.dateTime.year()).description ?? + .emptyDash + + return "\(name) (\(year))" + } } } diff --git a/Swiftfin/Views/ItemEditorView/ItemIdentification/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/ItemIdentificationView.swift similarity index 90% rename from Swiftfin/Views/ItemEditorView/ItemIdentification/IdentifyItemView.swift rename to Swiftfin/Views/ItemEditorView/ItemIdentification/ItemIdentificationView.swift index 2e34539f7..0931a66a6 100644 --- a/Swiftfin/Views/ItemEditorView/ItemIdentification/IdentifyItemView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemIdentification/ItemIdentificationView.swift @@ -10,7 +10,7 @@ import Combine import JellyfinAPI import SwiftUI -struct ItemIdentifyView: View { +struct ItemIdentificationView: View { // MARK: - Observed & Environment Objects @@ -70,10 +70,7 @@ struct ItemIdentifyView: View { set: { if !$0 { selectedMatch = nil } } )) { if let match = selectedMatch { - ItemInfoConfirmationView( - itemInfo: match, - remoteImage: resultImage(match.imageURL) - ) { + ItemInfoConfirmationView(itemInfo: match) { viewModel.send(.update(match)) selectedMatch = nil } onClose: { @@ -158,8 +155,7 @@ struct ItemIdentifyView: View { Section(L10n.items) { ForEach(viewModel.searchResults, id: \.id) { remoteSearchResult in RemoteSearchResultButton( - remoteSearchResult: remoteSearchResult, - remoteImage: resultImage(remoteSearchResult.imageURL) + remoteSearchResult: remoteSearchResult ) { selectedMatch = remoteSearchResult } @@ -171,16 +167,20 @@ struct ItemIdentifyView: View { // MARK: - Result Image @ViewBuilder - public func resultImage(_ url: String? = nil) -> some View { + static func resultImage(_ url: String? = nil) -> some View { ZStack { Color.clear ImageView(URL(string: url ?? "")) .failure { - SystemImageContentView(systemName: "questionmark") + Image(systemName: "questionmark") + .foregroundStyle(.primary) } } .posterStyle(.portrait) + .scaledToFit() + .frame(height: 120, alignment: .leading) .posterShadow() + .padding(.trailing) } } From 8420091f22d1ac24fb5bf7d9279298e12d067bea Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Dec 2024 14:00:58 -0700 Subject: [PATCH 07/13] wip --- .../Components/CancellableLoadingButton.swift | 48 ----- .../Coordinators/ItemEditorCoordinator.swift | 2 +- .../JellyfinAPI/RemoteSearchResult.swift | 29 ++- Shared/Extensions/Optional.swift | 4 + Shared/Extensions/String.swift | 8 + Shared/Extensions/URL.swift | 8 + Shared/Services/Notifications.swift | 5 +- ...odel.swift => IdentifyItemViewModel.swift} | 83 +++----- .../ItemViewModel/ItemViewModel.swift | 19 +- Swiftfin.xcodeproj/project.pbxproj | 50 ++--- Swiftfin/Components/ListRowButton.swift | 16 +- .../Components/RemoteSearchResultRow.swift | 58 ++++++ .../Components/RemoteSearchResultView.swift | 107 ++++++++++ .../IdentifyItemView/IdentifyItemView.swift | 191 ++++++++++++++++++ .../Components/ItemInfoConfirmationView.swift | 64 ------ .../Components/ItemInfoResultButton.swift | 61 ------ .../ItemIdentificationView.swift | 186 ----------------- .../Components/AboutView/AboutView.swift | 23 +-- Swiftfin/Views/ItemView/ItemView.swift | 2 +- .../VideoPlayer/Components/LoadingView.swift | 21 +- 20 files changed, 509 insertions(+), 476 deletions(-) delete mode 100644 Shared/Components/CancellableLoadingButton.swift rename Shared/ViewModels/ItemAdministration/{ItemIdentifyViewModel.swift => IdentifyItemViewModel.swift} (73%) create mode 100644 Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift create mode 100644 Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift create mode 100644 Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift delete mode 100644 Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift delete mode 100644 Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift delete mode 100644 Swiftfin/Views/ItemEditorView/ItemIdentification/ItemIdentificationView.swift diff --git a/Shared/Components/CancellableLoadingButton.swift b/Shared/Components/CancellableLoadingButton.swift deleted file mode 100644 index 326fd4631..000000000 --- a/Shared/Components/CancellableLoadingButton.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct CancellableLoadingButton: View { - - // MARK: - Button Variables - - private let description: String? - private let onCancel: () -> Void - - // MARK: - Initializer - - init(_ description: String?, onCancel: @escaping () -> Void) { - self.description = description - self.onCancel = onCancel - } - - // MARK: - Body - - var body: some View { - VStack(spacing: 10) { - if let description { - Text(description) - .foregroundColor(.primary) - } - - ProgressView() - .padding() - - Button(role: .cancel, action: onCancel) { - Text(L10n.cancel) - .foregroundColor(.red) - .padding() - .overlay { - Capsule() - .stroke(Color.red, lineWidth: 1) - } - } - } - } -} diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index de33776cc..70ea2d78e 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -64,7 +64,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @ViewBuilder func makeEditIdentity(item: BaseItemDto) -> some View { - ItemIdentificationView(item: item) + IdentifyItemView(item: item) } func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator { diff --git a/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift index b574f143a..9ea81b501 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift @@ -10,10 +10,33 @@ import Foundation import JellyfinAPI import SwiftUI -extension RemoteSearchResult: @retroactive Equatable, @retroactive Identifiable { +extension RemoteSearchResult: Displayable { - public var id: String { - UUID().uuidString + var displayTitle: String { + name ?? L10n.unknown + } +} + +// TODO: fix in SDK, should already be equatable +extension RemoteSearchResult: @retroactive Hashable, @retroactive Identifiable { + + public var id: Int { + hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(albumArtist) + hasher.combine(artists) + hasher.combine(imageURL) + hasher.combine(indexNumber) + hasher.combine(indexNumberEnd) + hasher.combine(name) + hasher.combine(overview) + hasher.combine(parentIndexNumber) + hasher.combine(premiereDate) + hasher.combine(productionYear) + hasher.combine(providerIDs) + hasher.combine(searchProviderName) } public static func == (lhs: RemoteSearchResult, rhs: RemoteSearchResult) -> Bool { diff --git a/Shared/Extensions/Optional.swift b/Shared/Extensions/Optional.swift index fff501843..82234f9a5 100644 --- a/Shared/Extensions/Optional.swift +++ b/Shared/Extensions/Optional.swift @@ -10,6 +10,10 @@ import Foundation extension Optional where Wrapped: Collection { + var isNilOrEmpty: Bool { + self?.isEmpty ?? true + } + mutating func appendedOrInit(_ element: Wrapped.Element) -> [Wrapped.Element] { if let self { return self + [element] diff --git a/Shared/Extensions/String.swift b/Shared/Extensions/String.swift index a19d80791..cffd79c2e 100644 --- a/Shared/Extensions/String.swift +++ b/Shared/Extensions/String.swift @@ -35,6 +35,14 @@ extension String { self + String(element) } + func appending(_ element: @autoclosure () -> String, if condition: Bool) -> String { + if condition { + return self + element() + } else { + return self + } + } + func prepending(_ element: String) -> String { element + self } diff --git a/Shared/Extensions/URL.swift b/Shared/Extensions/URL.swift index 8802c353c..e11a938e3 100644 --- a/Shared/Extensions/URL.swift +++ b/Shared/Extensions/URL.swift @@ -15,6 +15,14 @@ extension URL: Identifiable { } } +extension URL { + + init?(string: String?) { + guard let string = string else { return nil } + self.init(string: string) + } +} + extension URL { static var documents: URL { diff --git a/Shared/Services/Notifications.swift b/Shared/Services/Notifications.swift index 003287fec..35794ff95 100644 --- a/Shared/Services/Notifications.swift +++ b/Shared/Services/Notifications.swift @@ -124,12 +124,15 @@ extension Notifications.Key { // MARK: - Media Items + // TODO: come up with a cleaner, more defined way for item update notifications + /// - Payload: The new item with updated metadata. static var itemMetadataDidChange: Key { Key("itemMetadataDidChange") } - static var itemShouldRefresh: Key<(itemID: String, parentID: String?)> { + /// - Payload: The ID of the item that should refresh + static var itemShouldRefreshMetadata: Key { Key("itemShouldRefresh") } diff --git a/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift b/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift similarity index 73% rename from Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift rename to Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift index 83a0edf07..55e3419bb 100644 --- a/Shared/ViewModels/ItemAdministration/ItemIdentifyViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift @@ -12,7 +12,7 @@ import Get import JellyfinAPI import OrderedCollections -class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { +class IdentifyItemViewModel: ViewModel, Stateful, Eventful { // MARK: - Events @@ -30,28 +30,20 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { case update(RemoteSearchResult) } - // MARK: BackgroundState - - enum BackgroundState: Hashable { - case searching - case refreshing - } - // MARK: - State enum State: Hashable { - case initial + case content + case searching case updating } - @Published - var backgroundStates: OrderedSet = [] @Published var item: BaseItemDto @Published var searchResults: [RemoteSearchResult] = [] @Published - var state: State = .initial + var state: State = .content private var updateTask: AnyCancellable? private var searchTask: AnyCancellable? @@ -59,7 +51,9 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { private let eventSubject = PassthroughSubject() var events: AnyPublisher { - eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher() + eventSubject + .receive(on: RunLoop.main) + .eraseToAnyPublisher() } // MARK: - Initializer @@ -78,69 +72,54 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { updateTask?.cancel() searchTask?.cancel() - self.backgroundStates = [] - self.state = .initial - - return state + return .content case let .search(name, originalTitle, year): searchTask?.cancel() - searchTask = Task { [weak self] in - guard let self else { return } - + searchTask = Task { do { - await MainActor.run { - _ = self.backgroundStates.append(.searching) - } - - let allElements = try await self.searchItem( + let newResults = try await self.searchItem( name: name, originalTitle: originalTitle, year: year ) await MainActor.run { - self.searchResults = allElements - _ = self.backgroundStates.remove(.searching) + self.searchResults = newResults + self.state = .content } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .initial + self.state = .content self.eventSubject.send(.error(apiError)) } } }.asAnyCancellable() - return state + return .searching case let .update(searchResult): updateTask?.cancel() - updateTask = Task { [weak self] in - guard let self else { return } - + updateTask = Task { do { - await MainActor.run { - self.state = .updating - } - try await updateItem(searchResult) await MainActor.run { - self.state = .initial + self.state = .content self.eventSubject.send(.updated) } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .initial + self.state = .content self.eventSubject.send(.error(apiError)) } } }.asAnyCancellable() - return state + return .updating } } @@ -152,14 +131,14 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { year: Int? ) async throws -> [RemoteSearchResult] { - guard let itemId = item.id, let itemType = item.type else { + guard let itemID = item.id, let itemType = item.type else { return [] } switch itemType { case .boxSet: let parameters = BoxSetInfoRemoteSearchQuery( - itemID: itemId, + itemID: itemID, searchInfo: BoxSetInfo( name: name, originalTitle: originalTitle, @@ -173,7 +152,7 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { case .movie: let parameters = MovieInfoRemoteSearchQuery( - itemID: itemId, + itemID: itemID, searchInfo: MovieInfo( name: name, originalTitle: originalTitle, @@ -187,7 +166,7 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { case .person: let parameters = PersonLookupInfoRemoteSearchQuery( - itemID: itemId, + itemID: itemID, searchInfo: PersonLookupInfo( name: name, originalTitle: originalTitle, @@ -201,7 +180,7 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { case .series: let parameters = SeriesInfoRemoteSearchQuery( - itemID: itemId, + itemID: itemID, searchInfo: SeriesInfo( name: name, originalTitle: originalTitle, @@ -221,9 +200,9 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { // MARK: - Save Updated Item to Server private func updateItem(_ match: RemoteSearchResult) async throws { - guard let itemId = item.id else { return } + guard let itemID = item.id else { return } - let request = Paths.applySearchCriteria(itemID: itemId, match) + let request = Paths.applySearchCriteria(itemID: itemID, match) _ = try await userSession.client.send(request) try await refreshItem() @@ -232,20 +211,14 @@ class ItemIdentifyViewModel: ViewModel, Stateful, Eventful { // MARK: - Refresh Item private func refreshItem() async throws { - guard let itemId = item.id else { return } + guard let itemID = item.id else { return } - await MainActor.run { - _ = self.backgroundStates.append(.refreshing) - } - - let request = Paths.getItem(userID: userSession.user.id, itemID: itemId) + let request = Paths.getItem(userID: userSession.user.id, itemID: itemID) let response = try await userSession.client.send(request) await MainActor.run { self.item = response.value - _ = self.backgroundStates.remove(.refreshing) - - Notifications[.itemMetadataDidChange].post(item) + Notifications[.itemShouldRefreshMetadata].post(itemID) } } } diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index cebab7706..80dc78553 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -14,6 +14,8 @@ import JellyfinAPI import OrderedCollections import UIKit +// TODO: come up with a cleaner, more defined way for item update notifications + class ItemViewModel: ViewModel, Stateful { // MARK: Action @@ -89,10 +91,10 @@ class ItemViewModel: ViewModel, Stateful { self.item = item super.init() - Notifications[.itemShouldRefresh] + Notifications[.itemShouldRefreshMetadata] .publisher - .sink { itemID, parentID in - guard itemID == self.item.id || parentID == self.item.id else { return } + .sink { itemID in + guard itemID == self.item.id else { return } Task { await self.send(.backgroundRefresh) @@ -141,9 +143,16 @@ class ItemViewModel: ViewModel, Stateful { await MainActor.run { self.backgroundStates.remove(.refresh) - self.item = results.fullItem + + // see TODO, as the item will be set in + // itemMetadataDidChange notification but + // is a bit redundant +// self.item = results.fullItem + self.similarItems = results.similarItems self.specialFeatures = results.specialFeatures + + Notifications[.itemMetadataDidChange].post(results.fullItem) } } catch { guard !Task.isCancelled else { return } @@ -332,7 +341,7 @@ class ItemViewModel: ViewModel, Stateful { } let _ = try await userSession.client.send(request) - Notifications[.itemShouldRefresh].post((itemID, nil)) + Notifications[.itemShouldRefreshMetadata].post(itemID) } private func setIsFavorite(_ isFavorite: Bool) async throws { diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 62ab4117a..1a053995f 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -67,8 +67,6 @@ 4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; }; - 4E37661B2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */; }; - 4E37661C2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */; }; 4E3A24DA2CFE34A00083A72C /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */; }; 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; @@ -218,13 +216,13 @@ 4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; }; - 4EE766F52D131FBC009658F0 /* ItemIdentificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F42D131FB7009658F0 /* ItemIdentificationView.swift */; }; - 4EE766F72D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */; }; - 4EE766F82D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */; }; + 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */; }; + 4EE766F72D132054009658F0 /* IdentifyItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* IdentifyItemViewModel.swift */; }; + 4EE766F82D132054009658F0 /* IdentifyItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* IdentifyItemViewModel.swift */; }; 4EE766FA2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; - 4EE767082D13403F009658F0 /* ItemInfoResultButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */; }; - 4EE7670A2D135CBA009658F0 /* ItemInfoConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */; }; + 4EE767082D13403F009658F0 /* RemoteSearchResultRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */; }; + 4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */; }; 4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; }; 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; }; 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; }; @@ -1230,7 +1228,6 @@ 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = ""; }; 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = ""; }; 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = ""; }; - 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableLoadingButton.swift; sourceTree = ""; }; 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsSection.swift; sourceTree = ""; }; 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInput.swift; sourceTree = ""; }; 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = ""; }; @@ -1349,11 +1346,11 @@ 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = ""; }; - 4EE766F42D131FB7009658F0 /* ItemIdentificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIdentificationView.swift; sourceTree = ""; }; - 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIdentifyViewModel.swift; sourceTree = ""; }; + 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemView.swift; sourceTree = ""; }; + 4EE766F62D132043009658F0 /* IdentifyItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemViewModel.swift; sourceTree = ""; }; 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResult.swift; sourceTree = ""; }; - 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoResultButton.swift; sourceTree = ""; }; - 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoConfirmationView.swift; sourceTree = ""; }; + 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResultRow.swift; sourceTree = ""; }; + 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResultView.swift; sourceTree = ""; }; 4EED87462CBF824B002354D2 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = ""; }; 4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; }; @@ -2515,9 +2512,9 @@ children = ( 4E8F74A62CE03D4C00CC8969 /* Components */, 4E6619FF2CEFE39000025C99 /* EditMetadataView */, - 4E3766192D2144BA00C5D7A5 /* ItemElements */, - 4EE766F32D131F6E009658F0 /* ItemIdentification */, + 4EE766F32D131F6E009658F0 /* IdentifyItemView */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, + 4E3766192D2144BA00C5D7A5 /* ItemElements */, ); path = ItemEditorView; sourceTree = ""; @@ -2535,7 +2532,7 @@ children = ( 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, - 4EE766F62D132043009658F0 /* ItemIdentifyViewModel.swift */, + 4EE766F62D132043009658F0 /* IdentifyItemViewModel.swift */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, ); path = ItemAdministration; @@ -2793,20 +2790,20 @@ path = EditAccessScheduleView; sourceTree = ""; }; - 4EE766F32D131F6E009658F0 /* ItemIdentification */ = { + 4EE766F32D131F6E009658F0 /* IdentifyItemView */ = { isa = PBXGroup; children = ( 4EE767062D13401C009658F0 /* Components */, - 4EE766F42D131FB7009658F0 /* ItemIdentificationView.swift */, + 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */, ); - path = ItemIdentification; + path = IdentifyItemView; sourceTree = ""; }; 4EE767062D13401C009658F0 /* Components */ = { isa = PBXGroup; children = ( - 4EE767092D135CAC009658F0 /* ItemInfoConfirmationView.swift */, - 4EE767072D134020009658F0 /* ItemInfoResultButton.swift */, + 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */, + 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */, ); path = Components; sourceTree = ""; @@ -4458,7 +4455,6 @@ E104DC952B9E7E29008F506D /* AssertionFailureView.swift */, E18E0203288749200022598C /* BlurView.swift */, E145EB212BDCCA43003BF6F3 /* BulletedList.swift */, - 4E37661A2D21498D00C5D7A5 /* CancellableLoadingButton.swift */, 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */, E1A1528728FD229500600579 /* ChevronButton.swift */, E1153DCB2BBB633B00424D36 /* FastSVGView.swift */, @@ -5307,7 +5303,7 @@ E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */, E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */, E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */, - 4EE766F82D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */, + 4EE766F82D132054009658F0 /* IdentifyItemViewModel.swift in Sources */, E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */, E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */, BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */, @@ -5358,7 +5354,6 @@ 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, E10B1EC22BD9AD6100A92EAF /* V1UserModel.swift in Sources */, E1E2F8432B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */, - 4E37661B2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */, E111D8F628D03B7500400001 /* PagingLibraryViewModel.swift in Sources */, E1575E87293E7A00001665B1 /* InvertedDarkAppIcon.swift in Sources */, E10432F72BE4426F006FF9DD /* FormatStyle.swift in Sources */, @@ -5724,7 +5719,7 @@ 4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */, 4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */, 4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */, - 4EE766F52D131FBC009658F0 /* ItemIdentificationView.swift in Sources */, + 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */, 4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */, 4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */, 4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */, @@ -5735,7 +5730,7 @@ E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */, - 4EE767082D13403F009658F0 /* ItemInfoResultButton.swift in Sources */, + 4EE767082D13403F009658F0 /* RemoteSearchResultRow.swift in Sources */, E1ED7FD92CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */, E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */, 4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */, @@ -5759,7 +5754,7 @@ E111D8F828D03BF900400001 /* PagingLibraryView.swift in Sources */, E187F7672B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, 4E10C8172CC0455A0012CC9F /* CompatibilitiesSection.swift in Sources */, - 4EE7670A2D135CBA009658F0 /* ItemInfoConfirmationView.swift in Sources */, + 4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */, E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */, E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */, @@ -5803,7 +5798,6 @@ E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */, 4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */, 4E2AC4CB2C6C494E00DD600D /* VideoCodec.swift in Sources */, - 4E37661C2D2149B800C5D7A5 /* CancellableLoadingButton.swift in Sources */, E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */, 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */, E18E0204288749200022598C /* RowDivider.swift in Sources */, @@ -6139,7 +6133,7 @@ E1AD105F26D9ADDD003E4A08 /* NameGuidPair.swift in Sources */, 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */, E18A8E7D28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, - 4EE766F72D132054009658F0 /* ItemIdentifyViewModel.swift in Sources */, + 4EE766F72D132054009658F0 /* IdentifyItemViewModel.swift in Sources */, 4EC2B19B2CC96E7400D866BE /* ServerUsersView.swift in Sources */, E18E01F1288747230022598C /* PlayButton.swift in Sources */, E129429028F0BDC300796AC6 /* TimeStampType.swift in Sources */, diff --git a/Swiftfin/Components/ListRowButton.swift b/Swiftfin/Components/ListRowButton.swift index da110b833..ebd108133 100644 --- a/Swiftfin/Components/ListRowButton.swift +++ b/Swiftfin/Components/ListRowButton.swift @@ -22,21 +22,23 @@ struct ListRowButton: View { } var body: some View { - Button(title) { - action() - } - .font(.body.weight(.bold)) - .buttonStyle(ListRowButtonStyle()) - .listRowInsets(.init(.zero)) + Button(title, action: action) + .font(.body.weight(.bold)) + .buttonStyle(ListRowButtonStyle()) + .listRowInsets(.init(.zero)) } } +// TODO: implement `role` private struct ListRowButtonStyle: ButtonStyle { + @Environment(\.isEnabled) + private var isEnabled + func makeBody(configuration: Configuration) -> some View { ZStack { Rectangle() - .foregroundStyle(.secondary) + .foregroundStyle(isEnabled ? AnyShapeStyle(HierarchicalShapeStyle.secondary) : AnyShapeStyle(Color.gray)) configuration.label .foregroundStyle(.primary) diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift new file mode 100644 index 000000000..11f42dd05 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift @@ -0,0 +1,58 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension IdentifyItemView { + + struct RemoteSearchResultRow: View { + + // MARK: - Remote Search Result Variable + + let result: RemoteSearchResult + + // MARK: - Remote Search Result Action + + let onSelect: () -> Void + + // MARK: - Body + + var body: some View { + ListRow { + IdentifyItemView.resultImage(URL(string: result.imageURL)) + .frame(width: 60) + } content: { + VStack(alignment: .leading) { + Text(resultTitle) + .font(.headline) + .foregroundStyle(.primary) + + if let overview = result.overview { + HStack { + Text(overview) + .lineLimit(3) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + } + } + .onSelect(perform: onSelect) + .isSeparatorVisible(false) + } + + // MARK: - Result Title + + private var resultTitle: String { + result.displayTitle + .appending(" (\(result.premiereDate!.formatted(.dateTime.year())))", if: result.premiereDate != nil) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift new file mode 100644 index 000000000..2122e4ad7 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift @@ -0,0 +1,107 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension IdentifyItemView { + + struct RemoteSearchResultView: View { + + // MARK: - Item Info Variables + + let result: RemoteSearchResult + + // MARK: - Item Info Actions + + let onSave: () -> Void + let onClose: () -> Void + + // MARK: - Body + + @ViewBuilder + private var header: some View { + Section { + HStack(alignment: .bottom, spacing: 12) { + IdentifyItemView.resultImage(URL(string: result.imageURL)) + .frame(width: 100) + .accessibilityIgnoresInvertColors() + + Text(result.displayTitle) + .font(.title2) + .fontWeight(.semibold) + .foregroundStyle(.primary) + .lineLimit(2) + .padding(.bottom) + } + } + .listRowBackground(Color.clear) + .listRowCornerRadius(0) + .listRowInsets(.zero) + } + + @ViewBuilder + private var resultDetails: some View { + Section(L10n.details) { + + if let premiereDate = result.premiereDate { + TextPairView( + "Premiere Date", + value: Text(premiereDate.formatted(.dateTime.year().month().day())) + ) + } + + if let productionYear = result.productionYear { + TextPairView( + "Production Year", + value: Text(productionYear, format: .number.grouping(.never)) + ) + } + + if let provider = result.searchProviderName { + TextPairView( + leading: "Provider", + trailing: provider + ) + } + + if let providerID = result.providerIDs?.values.first { + TextPairView( + leading: "ID", + trailing: providerID + ) + } + } + + if let overview = result.overview { + Section(L10n.overview) { + Text(overview) + } + } + } + + var body: some View { + NavigationView { + List { + header + + resultDetails + } + .navigationTitle(L10n.identify) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + onClose() + } + .topBarTrailing { + Button(L10n.save, action: onSave) + .buttonStyle(.toolbarPill) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift new file mode 100644 index 000000000..3f61017da --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -0,0 +1,191 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Defaults +import JellyfinAPI +import SwiftUI + +struct IdentifyItemView: View { + + private struct SearchFields: Equatable { + var name: String? + var originalTitle: String? + var year: Int? + + var isEmpty: Bool { + name.isNilOrEmpty && + originalTitle.isNilOrEmpty && + year == nil + } + } + + @Default(.accentColor) + private var accentColor + + @FocusState + private var isTitleFocused: Bool + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: ItemEditorCoordinator.Router + + @StateObject + private var viewModel: IdentifyItemViewModel + + // MARK: - Identity Variables + + @State + private var selectedResult: RemoteSearchResult? + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Lookup States + + @State + private var search = SearchFields() + + // MARK: - Initializer + + init(item: BaseItemDto) { + self._viewModel = StateObject(wrappedValue: IdentifyItemViewModel(item: item)) + } + + // MARK: - Body + + var body: some View { + Group { + switch viewModel.state { + case .content, .searching: + contentView + case .updating: + ProgressView() + } + } + .navigationTitle(L10n.identify) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(viewModel.state == .updating) + .sheet(item: $selectedResult) { result in + RemoteSearchResultView(result: result) { + selectedResult = nil + viewModel.send(.update(result)) + } onClose: { + selectedResult = nil + } + } + .onReceive(viewModel.events) { events in + switch events { + case let .error(eventError): + error = eventError + case .cancelled: + selectedResult = nil + case .updated: + router.pop() + } + } + .errorMessage($error) + .onFirstAppear { + isTitleFocused = true + } + } + + // MARK: - Content View + + @ViewBuilder + private var contentView: some View { + Form { + searchView + + resultsView + } + } + + // MARK: - Search View + + @ViewBuilder + private var searchView: some View { + Section(L10n.search) { + TextField( + L10n.title, + text: $search.name.coalesce("") + ) + .focused($isTitleFocused) + + TextField( + L10n.originalTitle, + text: $search.originalTitle.coalesce("") + ) + + TextField( + L10n.year, + text: $search.year + .map( + getter: { $0 == nil ? "" : "\($0!)" }, + setter: { Int($0) } + ) + ) + .keyboardType(.numberPad) + } + + if viewModel.state == .searching { + ListRowButton(L10n.cancel) { + viewModel.send(.cancel) + } + .foregroundStyle(.red, .red.opacity(0.2)) + } else { + ListRowButton(L10n.search) { + viewModel.send(.search( + name: search.name, + originalTitle: search.originalTitle, + year: search.year + )) + } + .disabled(search.isEmpty) + .foregroundStyle( + accentColor.overlayColor, + accentColor + ) + } + } + + // MARK: - Results View + + @ViewBuilder + private var resultsView: some View { + if viewModel.searchResults.isNotEmpty { + Section(L10n.items) { + ForEach(viewModel.searchResults) { result in + RemoteSearchResultRow(result: result) { + selectedResult = result + } + } + } + } + } + + // MARK: - Result Image + + @ViewBuilder + static func resultImage(_ url: URL?) -> some View { + ZStack { + Color.clear + + ImageView(url) + .failure { + Image(systemName: "questionmark") + .foregroundStyle(.primary) + } + } + .posterStyle(.portrait) + .posterShadow() + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift deleted file mode 100644 index f353c0869..000000000 --- a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoConfirmationView.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -extension ItemIdentificationView { - - struct ItemInfoConfirmationView: View { - - // MARK: - Item Info Variables - - let itemInfo: RemoteSearchResult - - // MARK: - Item Info Actions - - let onSave: () -> Void - let onClose: () -> Void - - // MARK: - Body - - var body: some View { - NavigationView { - VStack(alignment: .leading, spacing: 16) { - ItemIdentificationView.resultImage(itemInfo.imageURL) - - Text(itemInfo.name ?? L10n.unknown) - .foregroundStyle(Color.primary) - .font(.headline) - - Text(itemInfo.premiereDate?.formatted(.dateTime.year().month().day()) ?? .emptyDash) - .foregroundStyle(Color.primary) - .font(.subheadline) - - Text(itemInfo.overview ?? L10n.unknown) - .foregroundStyle(Color.secondary) - - Spacer() - - Text(itemInfo.searchProviderName ?? L10n.unknown) - .foregroundStyle(Color.secondary) - .frame(maxWidth: .infinity, alignment: .center) - } - .padding() - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(L10n.useAsItem.localizedCapitalized) - .navigationBarCloseButton { - onClose() - } - .topBarTrailing { - Button(L10n.save) { - onSave() - } - .buttonStyle(.toolbarPill) - } - } - } - } -} diff --git a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift deleted file mode 100644 index 8a486dfcf..000000000 --- a/Swiftfin/Views/ItemEditorView/ItemIdentification/Components/ItemInfoResultButton.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Combine -import JellyfinAPI -import SwiftUI - -extension ItemIdentificationView { - - struct RemoteSearchResultButton: View { - - // MARK: - Remote Search Result Variable - - let remoteSearchResult: RemoteSearchResult - - // MARK: - Remote Search Result Action - - let onSelect: () -> Void - - // MARK: - Body - - var body: some View { - Button(action: onSelect) { - HStack { - ItemIdentificationView.resultImage(remoteSearchResult.imageURL) - - VStack(alignment: .leading) { - Text( - resultTitle - ) - .font(.headline) - .foregroundStyle(Color.primary) - - HStack { - Text(remoteSearchResult.overview ?? L10n.unknown) - .lineLimit(3) - .font(.subheadline) - .foregroundStyle(Color.secondary) - } - } - } - } - } - - // MARK: - Result Title - - private var resultTitle: String { - let name = remoteSearchResult.name ?? L10n.unknown - let year = remoteSearchResult.productionYear?.description ?? - remoteSearchResult.premiereDate?.formatted(.dateTime.year()).description ?? - .emptyDash - - return "\(name) (\(year))" - } - } -} diff --git a/Swiftfin/Views/ItemEditorView/ItemIdentification/ItemIdentificationView.swift b/Swiftfin/Views/ItemEditorView/ItemIdentification/ItemIdentificationView.swift deleted file mode 100644 index 0931a66a6..000000000 --- a/Swiftfin/Views/ItemEditorView/ItemIdentification/ItemIdentificationView.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Combine -import JellyfinAPI -import SwiftUI - -struct ItemIdentificationView: View { - - // MARK: - Observed & Environment Objects - - @EnvironmentObject - private var router: ItemEditorCoordinator.Router - - @ObservedObject - private var viewModel: ItemIdentifyViewModel - - // MARK: - Identity Variables - - @State - private var selectedMatch: RemoteSearchResult? - - // MARK: - Error State - - @State - private var error: Error? - - // MARK: - Lookup Truple - - private struct searchComponents: Equatable { - var name: String? - var originalTitle: String? - var year: Int? - } - - // MARK: - Lookup States - - @State - private var search = searchComponents() - @State - private var lastSearch = searchComponents() - - // MARK: - Initializer - - init(item: BaseItemDto) { - self.viewModel = .init(item: item) - } - - // MARK: - Body - - @ViewBuilder - var body: some View { - Group { - switch viewModel.state { - case .updating: - updateView - case .initial: - contentView - } - } - .navigationBarTitle(L10n.identify) - .navigationBarTitleDisplayMode(.inline) - .sheet(isPresented: Binding( - get: { selectedMatch != nil }, - set: { if !$0 { selectedMatch = nil } } - )) { - if let match = selectedMatch { - ItemInfoConfirmationView(itemInfo: match) { - viewModel.send(.update(match)) - selectedMatch = nil - } onClose: { - selectedMatch = nil - } - } - } - .navigationBarBackButtonHidden(viewModel.state == .updating) - .onReceive(viewModel.events) { events in - switch events { - case let .error(eventError): - error = eventError - case .cancelled: - selectedMatch = nil - case .updated: - router.pop() - } - } - .errorMessage($error) - } - - // MARK: - Update View - - @ViewBuilder - var updateView: some View { - CancellableLoadingButton(L10n.applyingMediaInformation) { - viewModel.send(.cancel) - } - .foregroundStyle(.primary) - } - - // MARK: - Content View - - @ViewBuilder - var contentView: some View { - Form { - searchView - resultsView - } - .topBarTrailing { - Button(L10n.search) { - viewModel.send(.search( - name: search.name, - originalTitle: search.originalTitle, - year: search.year - )) - lastSearch = search - } - .buttonStyle(.toolbarPill) - .disabled(viewModel.state == .updating || search == lastSearch) - } - } - - // MARK: - Search View - - @ViewBuilder - private var searchView: some View { - Section(header: Text(L10n.search)) { - TextField(L10n.title, text: Binding( - get: { search.name ?? "" }, - set: { search.name = $0.isEmpty ? nil : $0 } - )) - - TextField(L10n.originalTitle, text: Binding( - get: { search.originalTitle ?? "" }, - set: { search.originalTitle = $0.isEmpty ? nil : $0 } - )) - - TextField(L10n.year, text: Binding( - get: { search.year?.description ?? "" }, - set: { search.year = $0.isEmpty ? nil : Int($0) } - )) - .keyboardType(.numberPad) - } - } - - // MARK: - Results View - - @ViewBuilder - private var resultsView: some View { - if viewModel.searchResults.isNotEmpty { - Section(L10n.items) { - ForEach(viewModel.searchResults, id: \.id) { remoteSearchResult in - RemoteSearchResultButton( - remoteSearchResult: remoteSearchResult - ) { - selectedMatch = remoteSearchResult - } - } - } - } - } - - // MARK: - Result Image - - @ViewBuilder - static func resultImage(_ url: String? = nil) -> some View { - ZStack { - Color.clear - - ImageView(URL(string: url ?? "")) - .failure { - Image(systemName: "questionmark") - .foregroundStyle(.primary) - } - } - .posterStyle(.portrait) - .scaledToFit() - .frame(height: 120, alignment: .leading) - .posterShadow() - .padding(.trailing) - } -} diff --git a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift index f3bfcb06a..78ba39dce 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift @@ -8,8 +8,8 @@ import CollectionHStack import Defaults +import IdentifiedCollections import JellyfinAPI -import OrderedCollections import SwiftUI // TODO: rename `AboutItemView` @@ -22,8 +22,7 @@ extension ItemView { struct AboutView: View { - private enum AboutViewItem: Hashable, Identifiable { - + private enum AboutViewItem: Identifiable { case image case overview case mediaSource(MediaSourceInfo) @@ -43,21 +42,14 @@ extension ItemView { } } - @Default(.accentColor) - private var accentColor - @ObservedObject var viewModel: ItemViewModel @State private var contentSize: CGSize = .zero - @State - private var items: OrderedSet - - init(viewModel: ItemViewModel) { - self.viewModel = viewModel - var items: OrderedSet = [ + private var items: [AboutViewItem] { + var items: [AboutViewItem] = [ .image, .overview, ] @@ -70,7 +62,11 @@ extension ItemView { items.append(.ratings) } - self._items = State(initialValue: items) + return items + } + + init(viewModel: ItemViewModel) { + self.viewModel = viewModel } // TODO: break out into a general solution for general use? @@ -161,6 +157,7 @@ extension ItemView { .scrollBehavior(.continuousLeadingEdge) } .trackingSize($contentSize) + .id(viewModel.item.hashValue) } } } diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 78aaf8b4f..56416934d 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -122,7 +122,7 @@ struct ItemView: View { } var body: some View { - WrappedView { + ZStack { switch viewModel.state { case .content: contentView diff --git a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift index d8112cbf4..90cbfb6be 100644 --- a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift @@ -20,10 +20,25 @@ extension VideoPlayer { ZStack { Color.black - CancellableLoadingButton(L10n.retrievingMediaInformation) { - router.dismissCoordinator() + VStack(spacing: 10) { + + Text(L10n.retrievingMediaInformation) + .foregroundColor(.white) + + ProgressView() + + Button { + router.dismissCoordinator() + } label: { + Text(L10n.cancel) + .foregroundColor(.red) + .padding() + .overlay { + Capsule() + .stroke(Color.red, lineWidth: 1) + } + } } - .foregroundStyle(.white) } } } From 52187296366ad1910a8f48209f847fc11087a380 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Dec 2024 14:01:32 -0700 Subject: [PATCH 08/13] fix tvOS --- .../VideoPlayer/Components/LoadingView.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift index d8112cbf4..90cbfb6be 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift @@ -20,10 +20,25 @@ extension VideoPlayer { ZStack { Color.black - CancellableLoadingButton(L10n.retrievingMediaInformation) { - router.dismissCoordinator() + VStack(spacing: 10) { + + Text(L10n.retrievingMediaInformation) + .foregroundColor(.white) + + ProgressView() + + Button { + router.dismissCoordinator() + } label: { + Text(L10n.cancel) + .foregroundColor(.red) + .padding() + .overlay { + Capsule() + .stroke(Color.red, lineWidth: 1) + } + } } - .foregroundStyle(.white) } } } From f0ecbecdd1c9fd917b6e9cc91eedddd1dc1782ef Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Dec 2024 14:02:28 -0700 Subject: [PATCH 09/13] wip --- Shared/Coordinators/ItemEditorCoordinator.swift | 4 ++-- Swiftfin/Views/ItemEditorView/ItemEditorView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 70ea2d78e..930f6bfe0 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -22,7 +22,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Route to Metadata @Route(.push) - var editIdentity = makeEditIdentity + var identifyItem = makeIdentifyItem @Route(.modal) var editMetadata = makeEditMetadata @@ -63,7 +63,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Item Metadata @ViewBuilder - func makeEditIdentity(item: BaseItemDto) -> some View { + func makeIdentifyItem(item: BaseItemDto) -> some View { IdentifyItemView(item: item) } diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index 19b8d8f3f..01cd7ee7f 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -101,7 +101,7 @@ struct ItemEditorView: View { if [.boxSet, .movie, .person, .series].contains(viewModel.item.type) { ChevronButton(L10n.identify) .onSelect { - router.route(to: \.editIdentity, viewModel.item) + router.route(to: \.identifyItem, viewModel.item) } } ChevronButton(L10n.metadata) From cbd384504395574cebf99eff4eb55d092dac260f Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Dec 2024 14:05:04 -0700 Subject: [PATCH 10/13] localize --- Shared/Strings/Strings.swift | 6 ++++++ .../Components/RemoteSearchResultView.swift | 8 ++++---- Translations/en.lproj/Localizable.strings | 9 +++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 0ccb64e40..9d0726798 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -604,6 +604,8 @@ internal enum L10n { internal static let home = L10n.tr("Localizable", "home", fallback: "Home") /// Hours internal static let hours = L10n.tr("Localizable", "hours", fallback: "Hours") + /// ID + internal static let id = L10n.tr("Localizable", "id", fallback: "ID") /// Identify internal static let identify = L10n.tr("Localizable", "identify", fallback: "Identify") /// Idle @@ -910,6 +912,8 @@ internal enum L10n { internal static let production = L10n.tr("Localizable", "production", fallback: "Production") /// Production Locations internal static let productionLocations = L10n.tr("Localizable", "productionLocations", fallback: "Production Locations") + /// Production Year + internal static let productionYear = L10n.tr("Localizable", "productionYear", fallback: "Production Year") /// Profile Image internal static let profileImage = L10n.tr("Localizable", "profileImage", fallback: "Profile Image") /// Profiles @@ -918,6 +922,8 @@ internal enum L10n { internal static let programs = L10n.tr("Localizable", "programs", fallback: "Programs") /// Progress internal static let progress = L10n.tr("Localizable", "progress", fallback: "Progress") + /// Provider + internal static let provider = L10n.tr("Localizable", "provider", fallback: "Provider") /// Public Users internal static let publicUsers = L10n.tr("Localizable", "publicUsers", fallback: "Public Users") /// Quick Connect diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift index 2122e4ad7..a08c49a14 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift @@ -51,28 +51,28 @@ extension IdentifyItemView { if let premiereDate = result.premiereDate { TextPairView( - "Premiere Date", + L10n.premiereDate, value: Text(premiereDate.formatted(.dateTime.year().month().day())) ) } if let productionYear = result.productionYear { TextPairView( - "Production Year", + L10n.productionYear, value: Text(productionYear, format: .number.grouping(.never)) ) } if let provider = result.searchProviderName { TextPairView( - leading: "Provider", + leading: L10n.provider, trailing: provider ) } if let providerID = result.providerIDs?.values.first { TextPairView( - leading: "ID", + leading: L10n.id, trailing: providerID ) } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 5c23bbfc1..02cdd7f0d 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -850,6 +850,9 @@ /// Hours "hours" = "Hours"; +/// ID +"id" = "ID"; + /// Identify "identify" = "Identify"; @@ -1297,6 +1300,9 @@ /// Production Locations "productionLocations" = "Production Locations"; +/// Production Year +"productionYear" = "Production Year"; + /// Profile Image "profileImage" = "Profile Image"; @@ -1309,6 +1315,9 @@ /// Progress "progress" = "Progress"; +/// Provider +"provider" = "Provider"; + /// Public Users "publicUsers" = "Public Users"; From 6e0a14b72ffb23aed3c70ce09c082ad8985cc5eb Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Dec 2024 14:07:02 -0700 Subject: [PATCH 11/13] Update RemoteSearchResultRow.swift --- .../Components/RemoteSearchResultRow.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift index 11f42dd05..f61607078 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift @@ -21,6 +21,13 @@ extension IdentifyItemView { // MARK: - Remote Search Result Action let onSelect: () -> Void + + // MARK: - Result Title + + private var resultTitle: String { + result.displayTitle + .appending(" (\(result.premiereDate!.formatted(.dateTime.year())))", if: result.premiereDate != nil) + } // MARK: - Body @@ -35,24 +42,15 @@ extension IdentifyItemView { .foregroundStyle(.primary) if let overview = result.overview { - HStack { - Text(overview) - .lineLimit(3) - .font(.subheadline) - .foregroundStyle(.secondary) - } + Text(overview) + .lineLimit(3) + .font(.subheadline) + .foregroundStyle(.secondary) } } } .onSelect(perform: onSelect) .isSeparatorVisible(false) } - - // MARK: - Result Title - - private var resultTitle: String { - result.displayTitle - .appending(" (\(result.premiereDate!.formatted(.dateTime.year())))", if: result.premiereDate != nil) - } } } From c9f12f111b5da0d4cc46cb54cf88a5735fca7853 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Dec 2024 14:08:01 -0700 Subject: [PATCH 12/13] Update Localizable.strings --- Translations/en.lproj/Localizable.strings | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 02cdd7f0d..e68488169 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -7,9 +7,6 @@ /// Accent Color "accentColor" = "Accent Color"; -/// Some views may need an app restart to update. -"accentColorDescription" = "Some views may need an app restart to update."; - /// Access "access" = "Access"; @@ -136,9 +133,6 @@ /// Application Name "applicationName" = "Application Name"; -/// Applying media information -"applyingMediaInformation" = "Applying media information"; - /// Arranger "arranger" = "Arranger"; @@ -1873,9 +1867,6 @@ /// URL "url" = "URL"; -/// Use as item -"useAsItem" = "Use as item"; - /// Use as Transcoding Profile "useAsTranscodingProfile" = "Use as Transcoding Profile"; From 0c548f486b3306aa1d821f475fb567d4a5383d27 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Dec 2024 14:09:23 -0700 Subject: [PATCH 13/13] Update RemoteSearchResultRow.swift --- .../IdentifyItemView/Components/RemoteSearchResultRow.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift index f61607078..11edd4665 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift @@ -21,7 +21,7 @@ extension IdentifyItemView { // MARK: - Remote Search Result Action let onSelect: () -> Void - + // MARK: - Result Title private var resultTitle: String {