From 305a9b5fb1e523cda76d8dfa02412853ec07f79e Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 7 Dec 2024 18:50:13 -0700 Subject: [PATCH 01/45] Good start but some missing items: - Upload image isn't working - Only a single image is shown per section. Need to make this the HCollection of all images for the group --- .../Coordinators/ItemEditorCoordinator.swift | 21 ++ Shared/Strings/Strings.swift | 4 +- .../RemoteItemImageViewModel.swift | 321 ++++++++++++++++++ Swiftfin.xcodeproj/project.pbxproj | 30 ++ .../EditItemImagesView.swift | 131 +++++++ .../Views/ItemEditorView/ItemEditorView.swift | 4 + .../ItemImagePickerView.swift | 211 ++++++++++++ 7 files changed, 720 insertions(+), 2 deletions(-) create mode 100644 Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift create mode 100644 Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 37e698eac..f56725db7 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -24,6 +24,13 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @Route(.modal) var editMetadata = makeEditMetadata + // MARK: - Route to Metadata + + @Route(.push) + var editImages = makeEditImages + @Route(.modal) + var imagePicker = makeImagePicker + // MARK: - Route to Genres @Route(.push) @@ -66,6 +73,20 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { } } + // MARK: - Item Images + + @ViewBuilder + func makeEditImages(item: BaseItemDto) -> some View { + EditItemImagesView(viewModel: ItemViewModel(item: item)) + } + + func makeImagePicker(viewModel: RemoteItemImageViewModel) + -> NavigationViewCoordinator { + NavigationViewCoordinator { + ItemImagePickerView(viewModel: viewModel) + } + } + // MARK: - Item Genres @ViewBuilder diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 8957324b8..bf8471758 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1086,10 +1086,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Runtime - internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Run Time internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") + /// Runtime + internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift new file mode 100644 index 000000000..73830ce94 --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift @@ -0,0 +1,321 @@ +// +// 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 +import OrderedCollections + +private let DefaultPageSize = 50 + +class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { + + enum Event: Equatable { + case updated + case error(JellyfinAPIError) + } + + enum Action: Equatable { + case refresh + case getNextPage + case setImage(imageURL: String? = nil, imageData: Data? = nil) + case deleteImage + } + + enum BackgroundState: Hashable { + case gettingNextPage + case refreshing + case updating + } + + enum State: Hashable { + case initial + case content + case error(JellyfinAPIError) + } + + @Published + var state: State = .initial + @Published + var backgroundStates: OrderedSet = [] + + @Published + var item: BaseItemDto + @Published + var imageType: ImageType + @Published + var imageIndex: Int? + @Published + var includeAllLanguages: Bool + @Published + var images: OrderedSet = [] + + private let pageSize: Int + private(set) var currentPage: Int = 0 + private(set) var hasNextPage: Bool = true + + private var task: AnyCancellable? + private let eventSubject = PassthroughSubject() + + var events: AnyPublisher { + eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher() + } + + // MARK: - Init + + init( + item: BaseItemDto, + imageType: ImageType, + includeAllLanguages: Bool = false, + imageIndex: Int? = nil, + pageSize: Int = DefaultPageSize + ) { + self.item = item + self.imageType = imageType + self.includeAllLanguages = includeAllLanguages + self.imageIndex = imageIndex + self.pageSize = pageSize + super.init() + } + + // MARK: - Respond to Actions + + func respond(to action: Action) -> State { + switch action { + case .refresh: + task?.cancel() + + task = Task { [weak self] in + guard let self else { return } + do { + await MainActor.run { + self.state = .initial + self.images.removeAll() + self.currentPage = 0 + self.hasNextPage = true + _ = self.backgroundStates.append(.refreshing) + } + + try await self.getNextPage() + + await MainActor.run { + self.state = .content + self.eventSubject.send(.updated) + _ = self.backgroundStates.remove(.refreshing) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + _ = self.backgroundStates.remove(.refreshing) + } + } + }.asAnyCancellable() + + return state + + case .getNextPage: + guard hasNextPage else { return .content } + task?.cancel() + task = Task { [weak self] in + guard let self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.gettingNextPage) + } + + try await self.getNextPage() + + await MainActor.run { + self.state = .content + self.eventSubject.send(.updated) + _ = self.backgroundStates.remove(.gettingNextPage) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + _ = self.backgroundStates.remove(.gettingNextPage) + } + } + }.asAnyCancellable() + + return state + + case let .setImage(imageURL, imageData): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + } + + try await self.setImage( + self.imageType, + imageURL: imageURL, + imageData: imageData, + index: self.imageIndex + ) + + await MainActor.run { + self.eventSubject.send(.updated) + _ = self.backgroundStates.remove(.updating) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + _ = self.backgroundStates.remove(.updating) + } + } + }.asAnyCancellable() + + return state + + case .deleteImage: + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + } + + try await self.deleteImage(self.imageType, index: self.imageIndex) + + await MainActor.run { + self.eventSubject.send(.updated) + _ = self.backgroundStates.remove(.updating) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + _ = self.backgroundStates.remove(.updating) + } + } + }.asAnyCancellable() + + return state + } + } + + // MARK: - Paging Logic + + private func getNextPage() async throws { + guard let itemID = item.id, hasNextPage else { return } + + let startIndex = currentPage * pageSize + let parameters = Paths.GetRemoteImagesParameters( + type: imageType, + startIndex: startIndex, + limit: pageSize, + isIncludeAllLanguages: includeAllLanguages + ) + + let request = Paths.getRemoteImages(itemID: itemID, parameters: parameters) + let response = try await userSession.client.send(request) + let fetchedImages = response.value.images ?? [] + + hasNextPage = fetchedImages.count >= pageSize + + await MainActor.run { + images.append(contentsOf: fetchedImages) + currentPage += 1 + } + } + + // MARK: - Set Image + + private func setImage( + _ type: ImageType, + imageURL: String? = nil, + imageData: Data? = nil, + index: Int? = nil + ) async throws { + + guard let itemID = item.id else { return } + + var uploadData: Data? + + if let imageData { + uploadData = imageData + } else if let imageURL { + let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: imageURL) + let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) + let response = try await userSession.client.send(imageRequest) + + uploadData = response.data + } + + if let imageData = uploadData { + if let index { + let updateRequest = Paths.setItemImageByIndex( + itemID: itemID, + imageType: type.rawValue, + imageIndex: index, + imageData + ) + _ = try await userSession.client.send(updateRequest) + } else { + let updateRequest = Paths.setItemImage( + itemID: itemID, + imageType: type.rawValue, + imageData + ) + _ = try await userSession.client.send(updateRequest) + } + } else { + throw JellyfinAPIError("No image data provided or downloaded.") + } + + try await refreshItem() + } + + // MARK: - Delete Image + + private func deleteImage(_ type: ImageType, index: Int?) async throws { + guard let itemID = item.id else { return } + + if let index { + let request = Paths.deleteItemImageByIndex(itemID: itemID, imageType: type.rawValue, imageIndex: index) + _ = try await userSession.client.send(request) + } else { + let request = Paths.deleteItemImage(itemID: itemID, imageType: type.rawValue) + _ = 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 { + _ = 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 + _ = backgroundStates.remove(.refreshing) + Notifications[.itemMetadataDidChange].post(object: item) + } + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index da6e05af3..9ff8bf209 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -66,6 +66,10 @@ 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; }; 4E3A24DA2CFE34A00083A72C /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */; }; 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; + 4E45939E2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */; }; + 4E45939F2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */; }; + 4E4593A32D04E2B500E277E1 /* EditItemImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */; }; + 4E4593A62D04E4E300E277E1 /* ItemImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A52D04E4DE00E277E1 /* ItemImagePickerView.swift */; }; 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */; }; 4E49DECF2CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */; }; @@ -1188,6 +1192,9 @@ 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.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 = ""; }; + 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemImageViewModel.swift; sourceTree = ""; }; + 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemImagesView.swift; sourceTree = ""; }; + 4E4593A52D04E4DE00E277E1 /* ItemImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePickerView.swift; sourceTree = ""; }; 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = ""; }; 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionSection.swift; sourceTree = ""; }; 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxBitratePolicy.swift; sourceTree = ""; }; @@ -2187,6 +2194,22 @@ path = PlaybackBitrate; sourceTree = ""; }; + 4E4593A12D04E2A200E277E1 /* EditItemImagesView */ = { + isa = PBXGroup; + children = ( + 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */, + ); + path = EditItemImagesView; + sourceTree = ""; + }; + 4E4593A42D04E4D600E277E1 /* ItemImagePickerView */ = { + isa = PBXGroup; + children = ( + 4E4593A52D04E4DE00E277E1 /* ItemImagePickerView.swift */, + ); + path = ItemImagePickerView; + sourceTree = ""; + }; 4E49DEDE2CE55F7F00352DCD /* Components */ = { isa = PBXGroup; children = ( @@ -2370,8 +2393,10 @@ 4E5071E22CFCEFC3003FA2AD /* AddItemElementView */, 4E8F74A62CE03D4C00CC8969 /* Components */, 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, + 4E4593A12D04E2A200E277E1 /* EditItemImagesView */, 4E6619FF2CEFE39000025C99 /* EditMetadataView */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, + 4E4593A42D04E4D600E277E1 /* ItemImagePickerView */, ); path = ItemEditorView; sourceTree = ""; @@ -2390,6 +2415,7 @@ 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, + 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */, ); path = ItemAdministration; sourceTree = ""; @@ -5221,6 +5247,7 @@ 4E8F74B12CE03EB000CC8969 /* RefreshMetadataViewModel.swift in Sources */, E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */, E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */, + 4E45939F2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */, E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */, @@ -5359,6 +5386,7 @@ E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */, E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */, E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */, + 4E4593A32D04E2B500E277E1 /* EditItemImagesView.swift in Sources */, 4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */, @@ -5473,6 +5501,7 @@ E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */, 4E8F74B22CE03EB000CC8969 /* RefreshMetadataViewModel.swift in Sources */, E129429B28F4A5E300796AC6 /* PlaybackSettingsView.swift in Sources */, + 4E4593A62D04E4E300E277E1 /* ItemImagePickerView.swift in Sources */, E1E9017B28DAAE4D001B1594 /* RoundedCorner.swift in Sources */, E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */, 4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */, @@ -5575,6 +5604,7 @@ E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */, 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */, + 4E45939E2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */, C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */, E18E01AD288746AF0022598C /* DotHStack.swift in Sources */, E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift new file mode 100644 index 000000000..cdbeb027a --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift @@ -0,0 +1,131 @@ +// +// 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 BlurHashKit +import CollectionVGrid +import Combine +import Defaults +import JellyfinAPI +import SwiftUI + +struct EditItemImagesView: View { + + @Default(.accentColor) + private var accentColor + + @EnvironmentObject + private var router: ItemEditorCoordinator.Router + + @ObservedObject + var viewModel: ItemViewModel + + // MARK: - Ordered Items + + private var orderedItems: [ImageType] { + ImageType.allCases.sorted { (lhs: ImageType, rhs: ImageType) in + if lhs == .primary && rhs != .primary { + return true + } else if lhs != .primary && rhs == .primary { + return false + } else { + return lhs.rawValue.localizedCaseInsensitiveCompare(rhs.rawValue) == .orderedAscending + } + } + } + + // MARK: - Body + + @ViewBuilder + var body: some View { + contentView + .navigationBarTitle("Images") + .navigationBarTitleDisplayMode(.inline) + } + + // MARK: - Content View + + private var contentView: some View { + ScrollView { + ForEach( + orderedItems, + id: \.self + ) { itemType in + Section { + imageButton(itemType) + Divider() + .padding(.vertical, 16) + } header: { + HStack(alignment: .center) { + Text(itemType.rawValue.localizedCapitalized) + Spacer() + } + .font(.headline) + } + .padding( + EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) + ) + } + } + } + + // MARK: - Image Button View + + private func imageButton(_ imageType: ImageType) -> some View { + Button { + router.route( + to: \.imagePicker, + RemoteItemImageViewModel( + item: viewModel.item, + imageType: imageType, + includeAllLanguages: false + ) + ) + } label: { + ZStack(alignment: .bottomTrailing) { + Color.secondarySystemFill + .frame(maxWidth: .infinity, maxHeight: .infinity) + + ZStack { + ImageView(viewModel.item.imageSource(imageType)) + .placeholder { source in + if let blurHash = source.blurHash { + BlurHashView(blurHash: blurHash, size: .Square(length: 8)) + .scaledToFit() + } else { + Image(systemName: "circle") + } + } + .failure { + VStack(spacing: 8) { + Image(systemName: "photo") + Text(L10n.none) + } + } + .foregroundColor(.secondary) + .font(.headline) + + VStack { + Spacer() + HStack { + Spacer() + Image(systemName: "pencil.circle.fill") + .resizable() + .frame(width: 30, height: 30) + .shadow(radius: 10) + .symbolRenderingMode(.palette) + .foregroundStyle(accentColor.overlayColor, accentColor) + .padding(8) + } + } + } + } + .scaledToFit() + .posterStyle(.landscape) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index 7a3920041..468579939 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -77,6 +77,10 @@ struct ItemEditorView: View { @ViewBuilder private var editView: some View { Section(L10n.edit) { + ChevronButton("Images") + .onSelect { + router.route(to: \.editImages, viewModel.item) + } ChevronButton(L10n.metadata) .onSelect { router.route(to: \.editMetadata, viewModel.item) diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift new file mode 100644 index 000000000..81b779989 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift @@ -0,0 +1,211 @@ +// +// 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 BlurHashKit +import CollectionVGrid +import Combine +import Defaults +import JellyfinAPI +import SwiftUI + +struct ItemImagePickerView: View { + + @Default(.accentColor) + private var accentColor + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + private var viewModel: RemoteItemImageViewModel + + @State + private var isPresentingConfirmation: Bool = false + @State + private var isPresentingDeletion: Bool = false + @State + private var isImportingFile: Bool = false + + @State + private var selectedImage: RemoteImageInfo? { + didSet { + if selectedImage != nil { + isPresentingConfirmation = true + } else { + isPresentingConfirmation = false + } + } + } + + init(viewModel: RemoteItemImageViewModel) { + self.viewModel = viewModel + } + + var body: some View { + contentView + .navigationBarTitle(viewModel.imageType.rawValue) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + router.dismissCoordinator() + } + .onFirstAppear { + if viewModel.state == .initial { + viewModel.send(.refresh) + } + } + .navigationBarMenuButton( + isLoading: viewModel.backgroundStates.contains { + $0 == .refreshing || $0 == .updating + } + ) { + Button(L10n.add, systemImage: "plus") { + isImportingFile = true + } + + Divider() + + Button(L10n.delete, systemImage: "trash", role: .destructive) { + isPresentingDeletion = true + } + } + .fileImporter( + isPresented: $isImportingFile, + allowedContentTypes: [.image], + allowsMultipleSelection: false + ) { result in + switch result { + case let .success(urls): + if let url = urls.first { + guard url.startAccessingSecurityScopedResource() else { + return + } + defer { url.stopAccessingSecurityScopedResource() } + + if let imageData = try? Data(contentsOf: url) { + viewModel.send(.setImage(imageData: imageData)) + router.dismissCoordinator() + } + } + case .failure: + break + } + } + .confirmationDialog( + L10n.save, + isPresented: $isPresentingConfirmation, + titleVisibility: .visible + ) { + Button(L10n.confirm) { + if let newImageURL = selectedImage?.url { + viewModel.send(.setImage(imageURL: newImageURL)) + } + selectedImage = nil + router.dismissCoordinator() + } + Button(L10n.cancel, role: .cancel) { + selectedImage = nil + } + } + .confirmationDialog( + L10n.delete, + isPresented: $isPresentingDeletion, + titleVisibility: .visible + ) { + Button(L10n.delete, role: .destructive) { + viewModel.send(.deleteImage) + isPresentingDeletion = false + router.dismissCoordinator() + } + Button(L10n.cancel, role: .cancel) { + isPresentingDeletion = false + } + } + } + + @ViewBuilder + private var contentView: some View { + switch viewModel.state { + case .initial: + DelayedProgressView() + case .content: + gridView + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } + } + } + + @ViewBuilder + private var gridView: some View { + if viewModel.images.isEmpty { + Text(L10n.none) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .listRowSeparator(.hidden) + .listRowInsets(.zero) + } else { + CollectionVGrid( + viewModel.images, + layout: .minWidth(150) + ) { image in + imageButton(image) + .padding(.vertical, 4) + } + .onReachedBottomEdge(offset: .offset(300)) { + viewModel.send(.getNextPage) + } + } + } + + private func imageButton(_ image: RemoteImageInfo?) -> some View { + Button { + selectedImage = image + } label: { + VStack { + posterImage(image) + + if let imageWidth = image?.width, let imageHeight = image?.height { + Text("\(imageWidth) x \(imageHeight)") + .font(.body) + } + + Text(image?.providerName ?? .emptyDash) + .font(.caption) + } + .foregroundStyle(Color.secondary) + } + } + + private func posterImage(_ posterImageInfo: RemoteImageInfo?) -> some View { + ZStack { + Color.secondarySystemFill + .frame(maxWidth: .infinity, maxHeight: .infinity) + + ImageView(URL(string: posterImageInfo?.url ?? "")) + .placeholder { source in + if let blurHash = source.blurHash { + BlurHashView(blurHash: blurHash, size: .Square(length: 8)) + .scaledToFit() + } else { + Image(systemName: "circle") + } + } + .failure { + VStack(spacing: 8) { + Image(systemName: "photo") + Text(L10n.none) + } + } + .foregroundColor(.secondary) + .font(.headline) + } + .scaledToFit() + .posterStyle(.landscape) + } +} From 8b813baea1eeb2375524614b0d3b6135b8232238 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 7 Dec 2024 21:03:46 -0700 Subject: [PATCH 02/45] Upload still failing but now update and set are 2 different processes because I think that's better. Spacing on the add screen is still all wrong but we're getting closer --- .../RemoteItemImageViewModel.swift | 163 ++++++++++++------ .../ItemImagePickerView.swift | 35 ++-- 2 files changed, 137 insertions(+), 61 deletions(-) diff --git a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift index 73830ce94..03230422a 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift @@ -8,8 +8,11 @@ import Combine import Foundation +import Get import JellyfinAPI import OrderedCollections +import UIKit +import URLQueryEncoder private let DefaultPageSize = 50 @@ -23,7 +26,8 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { enum Action: Equatable { case refresh case getNextPage - case setImage(imageURL: String? = nil, imageData: Data? = nil) + case setImage(url: String) + case uploadImage(image: UIImage) case deleteImage } @@ -105,7 +109,6 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { await MainActor.run { self.state = .content - self.eventSubject.send(.updated) _ = self.backgroundStates.remove(.refreshing) } } catch { @@ -134,7 +137,6 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { await MainActor.run { self.state = .content - self.eventSubject.send(.updated) _ = self.backgroundStates.remove(.gettingNextPage) } } catch { @@ -149,7 +151,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { return state - case let .setImage(imageURL, imageData): + case let .setImage(url): task?.cancel() task = Task { [weak self] in @@ -159,12 +161,35 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = self.backgroundStates.append(.updating) } - try await self.setImage( - self.imageType, - imageURL: imageURL, - imageData: imageData, - index: self.imageIndex - ) + try await self.setImage(url, index: self.imageIndex) + + await MainActor.run { + self.eventSubject.send(.updated) + _ = self.backgroundStates.remove(.updating) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + _ = self.backgroundStates.remove(.updating) + } + } + }.asAnyCancellable() + + return state + + case let .uploadImage(image): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + } + + try await self.uploadImage(image, index: self.imageIndex) await MainActor.run { self.eventSubject.send(.updated) @@ -192,7 +217,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = self.backgroundStates.append(.updating) } - try await self.deleteImage(self.imageType, index: self.imageIndex) + try await self.deleteImage(index: self.imageIndex) await MainActor.run { self.eventSubject.send(.updated) @@ -239,64 +264,99 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { // MARK: - Set Image - private func setImage( - _ type: ImageType, - imageURL: String? = nil, - imageData: Data? = nil, - index: Int? = nil - ) async throws { - + private func setImage(_ url: String, index: Int? = nil) async throws { guard let itemID = item.id else { return } - var uploadData: Data? + let parameters = Paths.DownloadRemoteImageParameters(type: imageType, imageURL: url) + let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) + let response = try await userSession.client.send(imageRequest) - if let imageData { - uploadData = imageData - } else if let imageURL { - let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: imageURL) - let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) - let response = try await userSession.client.send(imageRequest) + let imageData = response.data + var updateRequest: Request - uploadData = response.data + if let index { + updateRequest = Paths.setItemImageByIndex( + itemID: itemID, + imageType: imageType.rawValue, + imageIndex: index, + imageData + ) + } else { + updateRequest = Paths.setItemImage( + itemID: itemID, + imageType: imageType.rawValue, + imageData + ) } - if let imageData = uploadData { - if let index { - let updateRequest = Paths.setItemImageByIndex( - itemID: itemID, - imageType: type.rawValue, - imageIndex: index, - imageData - ) - _ = try await userSession.client.send(updateRequest) - } else { - let updateRequest = Paths.setItemImage( - itemID: itemID, - imageType: type.rawValue, - imageData - ) - _ = try await userSession.client.send(updateRequest) - } + _ = try await userSession.client.send(updateRequest) + + try await refreshItem() + } + + // MARK: - Upload Image + + private func uploadImage(_ image: UIImage, index: Int? = nil) async throws { + guard let itemID = item.id else { return } + + var contentType: String + var imageData: Data + var request: Request + + if let pngData = image.pngData() { + contentType = "image/png" + imageData = pngData + } else if let jpgData = image.jpegData(compressionQuality: 1) { + contentType = "image/jpeg" + imageData = jpgData } else { - throw JellyfinAPIError("No image data provided or downloaded.") + logger.error("Unable to convert given image to png/jpg") + throw JellyfinAPIError("An internal error occurred") } + if let index { + request = Paths.setItemImageByIndex( + itemID: itemID, + imageType: imageType.rawValue, + imageIndex: index, + imageData + ) + } else { + request = Paths.setItemImage( + itemID: itemID, + imageType: imageType.rawValue, + imageData + ) + } + + request.headers = ["Content-Type": contentType] + _ = try await userSession.client.send(request) + try await refreshItem() } // MARK: - Delete Image - private func deleteImage(_ type: ImageType, index: Int?) async throws { + private func deleteImage(index: Int?) async throws { guard let itemID = item.id else { return } + var request: Request + if let index { - let request = Paths.deleteItemImageByIndex(itemID: itemID, imageType: type.rawValue, imageIndex: index) - _ = try await userSession.client.send(request) + request = Paths.deleteItemImageByIndex( + itemID: itemID, + imageType: imageType.rawValue, + imageIndex: index + ) } else { - let request = Paths.deleteItemImage(itemID: itemID, imageType: type.rawValue) - _ = try await userSession.client.send(request) + request = Paths.deleteItemImage( + itemID: itemID, + imageType: imageType.rawValue + ) } + _ = try await userSession.client.send(request) + try await refreshItem() } @@ -309,7 +369,10 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = 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 { diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift index 81b779989..52600a2ba 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift @@ -73,6 +73,14 @@ struct ItemImagePickerView: View { isPresentingDeletion = true } } + .onReceive(viewModel.events) { event in + switch event { + case .updated: + router.dismissCoordinator() + case let .error(eventError): + break + } + } .fileImporter( isPresented: $isImportingFile, allowedContentTypes: [.image], @@ -81,14 +89,13 @@ struct ItemImagePickerView: View { switch result { case let .success(urls): if let url = urls.first { - guard url.startAccessingSecurityScopedResource() else { - return - } + guard url.startAccessingSecurityScopedResource() else { return } defer { url.stopAccessingSecurityScopedResource() } - if let imageData = try? Data(contentsOf: url) { - viewModel.send(.setImage(imageData: imageData)) - router.dismissCoordinator() + if let data = try? Data(contentsOf: url), + let newImage = UIImage(data: data) + { + viewModel.send(.uploadImage(image: newImage)) } } case .failure: @@ -101,8 +108,8 @@ struct ItemImagePickerView: View { titleVisibility: .visible ) { Button(L10n.confirm) { - if let newImageURL = selectedImage?.url { - viewModel.send(.setImage(imageURL: newImageURL)) + if let newURL = selectedImage?.url { + viewModel.send(.setImage(url: newURL)) } selectedImage = nil router.dismissCoordinator() @@ -168,7 +175,10 @@ struct ItemImagePickerView: View { selectedImage = image } label: { VStack { - posterImage(image) + posterImage( + image, + posterStyle: image?.height ?? 0 > image?.width ?? 0 ? .portrait : .landscape + ) if let imageWidth = image?.width, let imageHeight = image?.height { Text("\(imageWidth) x \(imageHeight)") @@ -182,7 +192,10 @@ struct ItemImagePickerView: View { } } - private func posterImage(_ posterImageInfo: RemoteImageInfo?) -> some View { + private func posterImage( + _ posterImageInfo: RemoteImageInfo?, + posterStyle: PosterDisplayType + ) -> some View { ZStack { Color.secondarySystemFill .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -206,6 +219,6 @@ struct ItemImagePickerView: View { .font(.headline) } .scaledToFit() - .posterStyle(.landscape) + .posterStyle(posterStyle) } } From 0699c807572cb76e857f56d3a489a4abd496a7b0 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 8 Dec 2024 13:41:24 -0700 Subject: [PATCH 03/45] ~70% Complete TODO: - Spacing for remote portrait images is wrong & cramped - Upload image from file browser never works & produces 400 error - Show all images for an item.imageType opposed to just the first - Setting image works but produces a 400 error - Error alert looks bad --- .../RemoteItemImageViewModel.swift | 44 +-- .../AddItemElementView.swift | 3 + .../EditItemImagesView.swift | 32 +-- .../ItemImagePickerView.swift | 250 +++++++++++------- 4 files changed, 189 insertions(+), 140 deletions(-) diff --git a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift index 03230422a..6a5a0d4a9 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift @@ -27,7 +27,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { case refresh case getNextPage case setImage(url: String) - case uploadImage(image: UIImage) + case uploadImage(image: Data) case deleteImage } @@ -170,7 +170,6 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .error(apiError) self.eventSubject.send(.error(apiError)) _ = self.backgroundStates.remove(.updating) } @@ -198,7 +197,6 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .error(apiError) self.eventSubject.send(.error(apiError)) _ = self.backgroundStates.remove(.updating) } @@ -226,7 +224,6 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .error(apiError) self.eventSubject.send(.error(apiError)) _ = self.backgroundStates.remove(.updating) } @@ -272,66 +269,49 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { let response = try await userSession.client.send(imageRequest) let imageData = response.data - var updateRequest: Request if let index { - updateRequest = Paths.setItemImageByIndex( + let updateRequest = Paths.setItemImageByIndex( itemID: itemID, imageType: imageType.rawValue, imageIndex: index, imageData ) + _ = try await userSession.client.send(updateRequest) } else { - updateRequest = Paths.setItemImage( + let updateRequest = Paths.setItemImage( itemID: itemID, imageType: imageType.rawValue, imageData ) + _ = try await userSession.client.send(updateRequest) } - _ = try await userSession.client.send(updateRequest) - try await refreshItem() } // MARK: - Upload Image - private func uploadImage(_ image: UIImage, index: Int? = nil) async throws { + private func uploadImage(_ image: Data, index: Int? = nil) async throws { guard let itemID = item.id else { return } - var contentType: String - var imageData: Data - var request: Request - - if let pngData = image.pngData() { - contentType = "image/png" - imageData = pngData - } else if let jpgData = image.jpegData(compressionQuality: 1) { - contentType = "image/jpeg" - imageData = jpgData - } else { - logger.error("Unable to convert given image to png/jpg") - throw JellyfinAPIError("An internal error occurred") - } - if let index { - request = Paths.setItemImageByIndex( + let request = Paths.setItemImageByIndex( itemID: itemID, imageType: imageType.rawValue, imageIndex: index, - imageData + image ) + _ = try await userSession.client.send(request) } else { - request = Paths.setItemImage( + let request = Paths.setItemImage( itemID: itemID, imageType: imageType.rawValue, - imageData + image ) + _ = try await userSession.client.send(request) } - request.headers = ["Content-Type": contentType] - _ = try await userSession.client.send(request) - try await refreshItem() } diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift b/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift index 825dcfde2..127aa35c8 100644 --- a/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift @@ -113,6 +113,9 @@ struct AddItemElementView: View { presenting: error ) { error in Text(error.localizedDescription) + Button(L10n.dismiss, role: .cancel) { + isPresentingError = false + } } } diff --git a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift index cdbeb027a..97a562019 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift @@ -91,23 +91,25 @@ struct EditItemImagesView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) ZStack { - ImageView(viewModel.item.imageSource(imageType)) - .placeholder { source in - if let blurHash = source.blurHash { - BlurHashView(blurHash: blurHash, size: .Square(length: 8)) - .scaledToFit() - } else { - Image(systemName: "circle") + RedrawOnNotificationView(.itemMetadataDidChange) { + ImageView(viewModel.item.imageSource(imageType)) + .placeholder { source in + if let blurHash = source.blurHash { + BlurHashView(blurHash: blurHash, size: .Square(length: 8)) + .scaledToFit() + } else { + Image(systemName: "circle") + } } - } - .failure { - VStack(spacing: 8) { - Image(systemName: "photo") - Text(L10n.none) + .failure { + VStack(spacing: 8) { + Image(systemName: "photo") + Text(L10n.none) + } } - } - .foregroundColor(.secondary) - .font(.headline) + .foregroundColor(.secondary) + .font(.headline) + } VStack { Spacer() diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift index 52600a2ba..e91ec7a88 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift @@ -15,14 +15,20 @@ import SwiftUI struct ItemImagePickerView: View { + // MARK: - Defaults and Environment + @Default(.accentColor) private var accentColor @EnvironmentObject private var router: BasicNavigationViewCoordinator.Router + // MARK: - ViewModel + @ObservedObject - private var viewModel: RemoteItemImageViewModel + var viewModel: RemoteItemImageViewModel + + // MARK: - Dialog States @State private var isPresentingConfirmation: Bool = false @@ -30,21 +36,26 @@ struct ItemImagePickerView: View { private var isPresentingDeletion: Bool = false @State private var isImportingFile: Bool = false + @State + private var isPresentingError: Bool = false + @State + private var error: Error? + + // MARK: - Selected Image @State private var selectedImage: RemoteImageInfo? { didSet { - if selectedImage != nil { - isPresentingConfirmation = true - } else { - isPresentingConfirmation = false - } + isPresentingConfirmation = selectedImage != nil } } - init(viewModel: RemoteItemImageViewModel) { - self.viewModel = viewModel - } + // MARK: - Collection Layout + + @State + private var layout: CollectionVGridLayout = .minWidth(150) + + // MARK: - Body var body: some View { contentView @@ -63,43 +74,24 @@ struct ItemImagePickerView: View { $0 == .refreshing || $0 == .updating } ) { - Button(L10n.add, systemImage: "plus") { - isImportingFile = true - } - - Divider() - - Button(L10n.delete, systemImage: "trash", role: .destructive) { - isPresentingDeletion = true - } + menuContent() } .onReceive(viewModel.events) { event in - switch event { - case .updated: - router.dismissCoordinator() - case let .error(eventError): - break - } + handleEvent(event) } .fileImporter( isPresented: $isImportingFile, allowedContentTypes: [.image], allowsMultipleSelection: false - ) { result in - switch result { - case let .success(urls): - if let url = urls.first { - guard url.startAccessingSecurityScopedResource() else { return } - defer { url.stopAccessingSecurityScopedResource() } - - if let data = try? Data(contentsOf: url), - let newImage = UIImage(data: data) - { - viewModel.send(.uploadImage(image: newImage)) - } - } - case .failure: - break + ) { handleFileImport($0) } + .alert( + L10n.error, + isPresented: $isPresentingError, + presenting: error + ) { error in + Text(error.localizedDescription) + Button(L10n.dismiss, role: .cancel) { + isPresentingError = false } } .confirmationDialog( @@ -107,33 +99,19 @@ struct ItemImagePickerView: View { isPresented: $isPresentingConfirmation, titleVisibility: .visible ) { - Button(L10n.confirm) { - if let newURL = selectedImage?.url { - viewModel.send(.setImage(url: newURL)) - } - selectedImage = nil - router.dismissCoordinator() - } - Button(L10n.cancel, role: .cancel) { - selectedImage = nil - } + confirmationDialogContent() } .confirmationDialog( L10n.delete, isPresented: $isPresentingDeletion, titleVisibility: .visible ) { - Button(L10n.delete, role: .destructive) { - viewModel.send(.deleteImage) - isPresentingDeletion = false - router.dismissCoordinator() - } - Button(L10n.cancel, role: .cancel) { - isPresentingDeletion = false - } + deletionDialogContent() } } + // MARK: - Content View + @ViewBuilder private var contentView: some View { switch viewModel.state { @@ -149,6 +127,8 @@ struct ItemImagePickerView: View { } } + // MARK: - Content Grid View + @ViewBuilder private var gridView: some View { if viewModel.images.isEmpty { @@ -159,7 +139,7 @@ struct ItemImagePickerView: View { } else { CollectionVGrid( viewModel.images, - layout: .minWidth(150) + layout: $layout ) { image in imageButton(image) .padding(.vertical, 4) @@ -170,55 +150,139 @@ struct ItemImagePickerView: View { } } + // MARK: - Poster Image Button + private func imageButton(_ image: RemoteImageInfo?) -> some View { Button { selectedImage = image } label: { - VStack { - posterImage( - image, - posterStyle: image?.height ?? 0 > image?.width ?? 0 ? .portrait : .landscape - ) - - if let imageWidth = image?.width, let imageHeight = image?.height { - Text("\(imageWidth) x \(imageHeight)") - .font(.body) - } - - Text(image?.providerName ?? .emptyDash) - .font(.caption) - } - .foregroundStyle(Color.secondary) + posterImage( + image, + posterStyle: image?.height ?? 0 > image?.width ?? 0 ? .portrait : .landscape + ) } } + // MARK: - Poster Image + private func posterImage( _ posterImageInfo: RemoteImageInfo?, posterStyle: PosterDisplayType ) -> some View { - ZStack { - Color.secondarySystemFill - .frame(maxWidth: .infinity, maxHeight: .infinity) - - ImageView(URL(string: posterImageInfo?.url ?? "")) - .placeholder { source in - if let blurHash = source.blurHash { - BlurHashView(blurHash: blurHash, size: .Square(length: 8)) - .scaledToFit() - } else { - Image(systemName: "circle") + VStack { + ZStack { + Color.secondarySystemFill + .frame(maxWidth: .infinity, maxHeight: .infinity) + + ImageView(URL(string: posterImageInfo?.url ?? "")) + .placeholder { source in + if let blurHash = source.blurHash { + BlurHashView(blurHash: blurHash, size: .Square(length: 8)) + .scaledToFit() + } else { + Image(systemName: "circle") + } } - } - .failure { - VStack(spacing: 8) { - Image(systemName: "photo") - Text(L10n.none) + .failure { + VStack(spacing: 8) { + Image(systemName: "photo") + Text(L10n.none) + } } + .foregroundColor(.secondary) + .font(.headline) + } + .scaledToFit() + .posterStyle(posterStyle) + + if let imageWidth = posterImageInfo?.width, let imageHeight = posterImageInfo?.height { + Text("\(imageWidth) x \(imageHeight)") + .font(.body) + } + + Text(posterImageInfo?.providerName ?? .emptyDash) + .font(.caption) + } + .foregroundStyle(Color.secondary) + } + + // MARK: - Set Image Dialog + + @ViewBuilder + private func confirmationDialogContent() -> some View { + Button(L10n.confirm) { + if let newURL = selectedImage?.url { + viewModel.send(.setImage(url: newURL)) + } + selectedImage = nil + } + Button(L10n.cancel, role: .cancel) { + selectedImage = nil + } + } + + // MARK: - Delete Image Dialog + + @ViewBuilder + private func deletionDialogContent() -> some View { + Button(L10n.delete, role: .destructive) { + viewModel.send(.deleteImage) + isPresentingDeletion = false + router.dismissCoordinator() + } + Button(L10n.cancel, role: .cancel) { + isPresentingDeletion = false + } + } + + // MARK: - Handle ViewModel Events + + private func handleEvent(_ event: RemoteItemImageViewModel.Event) { + switch event { + case .updated: + UIDevice.feedback(.success) + router.dismissCoordinator() + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + isPresentingError = true + } + } + + // MARK: - Handle File Importing + + private func handleFileImport(_ result: Result<[URL], Error>) { + switch result { + case let .success(urls): + if let url = urls.first { + guard url.startAccessingSecurityScopedResource() else { return } + defer { url.stopAccessingSecurityScopedResource() } + + do { + let fileData = try Data(contentsOf: url) + viewModel.send(.uploadImage(image: fileData)) + } catch { + self.error = error + isPresentingError = true } - .foregroundColor(.secondary) - .font(.headline) + } + case let .failure(fileError): + error = fileError + isPresentingError = true + } + } + + // MARK: - Navigation Menu Content + + private func menuContent() -> some View { + Group { + Button(L10n.add, systemImage: "plus") { + isImportingFile = true + } + Divider() + Button(L10n.delete, systemImage: "trash", role: .destructive) { + isPresentingDeletion = true + } } - .scaledToFit() - .posterStyle(posterStyle) } } From 480b94d0d65c0c9fd51d22a44a2cefcf1f99809b Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 8 Dec 2024 14:09:10 -0700 Subject: [PATCH 04/45] Merge with Main --- .../JellyfinAPI/RemoteImageInfo.swift | 20 +++++++++++++++++++ Shared/Strings/Strings.swift | 4 ++-- .../RemoteItemImageViewModel.swift | 3 ++- Swiftfin.xcodeproj/project.pbxproj | 6 ++++++ .../ItemImagePickerView.swift | 4 ++-- 5 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift new file mode 100644 index 000000000..7debc3ed2 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift @@ -0,0 +1,20 @@ +// +// 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 declares a conformance of imported type 'RemoteImageInfo' to imported protocol 'Identifiable'; this will not behave correctly +/// if the owners of 'JellyfinAPI' introduce this conformance in the future +extension RemoteImageInfo: Identifiable { + + public var id: String { + UUID().uuidString + } +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index bf8471758..8957324b8 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1086,10 +1086,10 @@ internal enum L10n { internal static let run = L10n.tr("Localizable", "run", fallback: "Run") /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") - /// Run Time - internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") + /// Run Time + internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Scan All Libraries diff --git a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift index 6a5a0d4a9..c06bb6a9f 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift @@ -9,6 +9,7 @@ import Combine import Foundation import Get +import IdentifiedCollections import JellyfinAPI import OrderedCollections import UIKit @@ -57,7 +58,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { @Published var includeAllLanguages: Bool @Published - var images: OrderedSet = [] + var images: IdentifiedArrayOf = [] private let pageSize: Int private(set) var currentPage: Int = 0 diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 5b5cc207a..a293684be 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; }; 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; }; 4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; }; + 4E1AA0042D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; }; + 4E1AA0052D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; }; 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; }; 4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; }; 4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; }; @@ -1171,6 +1173,7 @@ 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = ""; }; 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksView.swift; sourceTree = ""; }; 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = ""; }; + 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfo.swift; sourceTree = ""; }; 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = ""; }; 4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = ""; }; 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileAction.swift; sourceTree = ""; }; @@ -4221,6 +4224,7 @@ 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */, E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */, 4E2182E42CAF67EF0094806B /* PlayMethod.swift */, + 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */, 4E35CE652CBED8B300DBD886 /* ServerTicks.swift */, 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */, E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */, @@ -5295,6 +5299,7 @@ E18E021C2887492B0022598C /* BlurView.swift in Sources */, E187F7682B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, E1E6C44729AECD5D0064123F /* PlayPreviousItemActionButton.swift in Sources */, + 4E1AA0052D0640AA00524970 /* RemoteImageInfo.swift in Sources */, E1E6C44E29AEE9DC0064123F /* SmallMenuOverlay.swift in Sources */, E1CB75832C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */, E10B1ECB2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, @@ -5488,6 +5493,7 @@ E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */, 4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */, E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, + 4E1AA0042D0640AA00524970 /* RemoteImageInfo.swift in Sources */, 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift index e91ec7a88..c8e98f4a7 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift @@ -138,8 +138,8 @@ struct ItemImagePickerView: View { .listRowInsets(.zero) } else { CollectionVGrid( - viewModel.images, - layout: $layout + uniqueElements: viewModel.images, + layout: layout ) { image in imageButton(image) .padding(.vertical, 4) From f21bb9948e1176513858776bace23d97c5dca607 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 08:37:09 -0700 Subject: [PATCH 05/45] URL Changes --- Shared/Strings/Strings.swift | 2 - .../RemoteItemImageViewModel.swift | 93 +++++++++++-------- .../ItemImagePickerView.swift | 25 +---- 3 files changed, 54 insertions(+), 66 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index db5dde38b..472beedb9 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1198,8 +1198,6 @@ internal enum L10n { internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") - /// Run Time - internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Save the user to this device without any local authentication. diff --git a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift index c06bb6a9f..8fe3c9f44 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift @@ -28,7 +28,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { case refresh case getNextPage case setImage(url: String) - case uploadImage(image: Data) + case setLocalImage(url: URL) case deleteImage } @@ -54,7 +54,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { @Published var imageType: ImageType @Published - var imageIndex: Int? + var imageIndex: Int @Published var includeAllLanguages: Bool @Published @@ -77,7 +77,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { item: BaseItemDto, imageType: ImageType, includeAllLanguages: Bool = false, - imageIndex: Int? = nil, + imageIndex: Int = 0, pageSize: Int = DefaultPageSize ) { self.item = item @@ -179,7 +179,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { return state - case let .uploadImage(image): + case let .setLocalImage(url): task?.cancel() task = Task { [weak self] in @@ -189,7 +189,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = self.backgroundStates.append(.updating) } - try await self.uploadImage(image, index: self.imageIndex) + try await self.setLocalImage(url, index: self.imageIndex) await MainActor.run { self.eventSubject.send(.updated) @@ -260,59 +260,72 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { } } - // MARK: - Set Image + // MARK: - Set Image From URL - private func setImage(_ url: String, index: Int? = nil) async throws { + private func setImage(_ url: String, index: Int) async throws { guard let itemID = item.id else { return } let parameters = Paths.DownloadRemoteImageParameters(type: imageType, imageURL: url) let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) let response = try await userSession.client.send(imageRequest) - let imageData = response.data + let image = UIImage(data: response.data) - if let index { - let updateRequest = Paths.setItemImageByIndex( - itemID: itemID, - imageType: imageType.rawValue, - imageIndex: index, - imageData - ) - _ = try await userSession.client.send(updateRequest) - } else { - let updateRequest = Paths.setItemImage( - itemID: itemID, - imageType: imageType.rawValue, - imageData - ) - _ = try await userSession.client.send(updateRequest) + guard let image else { + logger.error("Unable to download the the selected image") + throw JellyfinAPIError("An internal error occurred") } - try await refreshItem() + try await uploadImage(image, index: index) + } + + // MARK: - Set Image From Local Files + + private func setLocalImage(_ url: URL, index: Int) async throws { + guard let itemID = item.id else { return } + guard url.startAccessingSecurityScopedResource() else { return } + defer { url.stopAccessingSecurityScopedResource() } + + let data = try Data(contentsOf: url) + let image = UIImage(data: data) + + guard let image else { + logger.error("Unable to download the the selected image") + throw JellyfinAPIError("An internal error occurred") + } + + try await uploadImage(image, index: index) } // MARK: - Upload Image - private func uploadImage(_ image: Data, index: Int? = nil) async throws { + private func uploadImage(_ image: UIImage, index: Int) async throws { guard let itemID = item.id else { return } - if let index { - let request = Paths.setItemImageByIndex( - itemID: itemID, - imageType: imageType.rawValue, - imageIndex: index, - image - ) - _ = try await userSession.client.send(request) + let contentType: String + let imageData: Data + + if let pngData = image.pngData()?.base64EncodedData() { + contentType = "image/png" + imageData = pngData + } else if let jpgData = image.jpegData(compressionQuality: 1)?.base64EncodedData() { + contentType = "image/jpeg" + imageData = jpgData } else { - let request = Paths.setItemImage( - itemID: itemID, - imageType: imageType.rawValue, - image - ) - _ = try await userSession.client.send(request) + logger.error("Unable to upload the the selected image") + throw JellyfinAPIError("An internal error occurred") } + var request = Paths.setItemImageByIndex( + itemID: itemID, + imageType: imageType.rawValue, + imageIndex: index, + imageData + ) + request.headers = ["Content-Type": contentType] + + _ = try await userSession.client.send(request) + try await refreshItem() } @@ -359,7 +372,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { await MainActor.run { self.item = response.value _ = backgroundStates.remove(.refreshing) - Notifications[.itemMetadataDidChange].post(object: item) + Notifications[.itemMetadataDidChange].post(item) } } } diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift index c8e98f4a7..226a1da91 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift @@ -37,8 +37,6 @@ struct ItemImagePickerView: View { @State private var isImportingFile: Bool = false @State - private var isPresentingError: Bool = false - @State private var error: Error? // MARK: - Selected Image @@ -84,16 +82,6 @@ struct ItemImagePickerView: View { allowedContentTypes: [.image], allowsMultipleSelection: false ) { handleFileImport($0) } - .alert( - L10n.error, - isPresented: $isPresentingError, - presenting: error - ) { error in - Text(error.localizedDescription) - Button(L10n.dismiss, role: .cancel) { - isPresentingError = false - } - } .confirmationDialog( L10n.save, isPresented: $isPresentingConfirmation, @@ -245,7 +233,6 @@ struct ItemImagePickerView: View { case let .error(eventError): UIDevice.feedback(.error) error = eventError - isPresentingError = true } } @@ -255,20 +242,10 @@ struct ItemImagePickerView: View { switch result { case let .success(urls): if let url = urls.first { - guard url.startAccessingSecurityScopedResource() else { return } - defer { url.stopAccessingSecurityScopedResource() } - - do { - let fileData = try Data(contentsOf: url) - viewModel.send(.uploadImage(image: fileData)) - } catch { - self.error = error - isPresentingError = true - } + viewModel.send(.setLocalImage(url: url)) } case let .failure(fileError): error = fileError - isPresentingError = true } } From fa291739d102a4b5bc77bd1c3f6b365b4950229c Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 12:05:25 -0700 Subject: [PATCH 06/45] Updating logic and confirmation screen --- .../JellyfinAPI/RemoteImageInfo.swift | 4 +- .../RemoteItemImageViewModel.swift | 44 ++--- .../EditItemImagesView.swift | 6 +- .../Views/ItemEditorView/ItemEditorView.swift | 2 +- .../ItemImagePickerView.swift | 186 +++++++++--------- 5 files changed, 125 insertions(+), 117 deletions(-) diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift index 7debc3ed2..467ddd8fe 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift @@ -10,9 +10,7 @@ import Foundation import JellyfinAPI import SwiftUI -/// Extension declares a conformance of imported type 'RemoteImageInfo' to imported protocol 'Identifiable'; this will not behave correctly -/// if the owners of 'JellyfinAPI' introduce this conformance in the future -extension RemoteImageInfo: Identifiable { +extension RemoteImageInfo: @retroactive Identifiable { public var id: String { UUID().uuidString diff --git a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift index 8fe3c9f44..e479d31ab 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift @@ -25,6 +25,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { } enum Action: Equatable { + case cancel case refresh case getNextPage case setImage(url: String) @@ -35,12 +36,12 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { enum BackgroundState: Hashable { case gettingNextPage case refreshing - case updating } enum State: Hashable { case initial case content + case updating case error(JellyfinAPIError) } @@ -92,6 +93,13 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { func respond(to action: Action) -> State { switch action { + + case .cancel: + task?.cancel() + self.state = .initial + + return state + case .refresh: task?.cancel() @@ -159,20 +167,20 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { guard let self = self else { return } do { await MainActor.run { - _ = self.backgroundStates.append(.updating) + _ = self.state = .updating } try await self.setImage(url, index: self.imageIndex) await MainActor.run { self.eventSubject.send(.updated) - _ = self.backgroundStates.remove(.updating) + _ = self.state = .updating } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { self.eventSubject.send(.error(apiError)) - _ = self.backgroundStates.remove(.updating) + _ = self.state = .updating } } }.asAnyCancellable() @@ -186,20 +194,20 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { guard let self = self else { return } do { await MainActor.run { - _ = self.backgroundStates.append(.updating) + _ = self.state = .updating } try await self.setLocalImage(url, index: self.imageIndex) await MainActor.run { self.eventSubject.send(.updated) - _ = self.backgroundStates.remove(.updating) + _ = self.state = .updating } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { self.eventSubject.send(.error(apiError)) - _ = self.backgroundStates.remove(.updating) + _ = self.state = .updating } } }.asAnyCancellable() @@ -213,20 +221,20 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { guard let self = self else { return } do { await MainActor.run { - _ = self.backgroundStates.append(.updating) + _ = self.state = .updating } try await self.deleteImage(index: self.imageIndex) await MainActor.run { self.eventSubject.send(.updated) - _ = self.backgroundStates.remove(.updating) + _ = self.state = .updating } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { self.eventSubject.send(.error(apiError)) - _ = self.backgroundStates.remove(.updating) + _ = self.state = .updating } } }.asAnyCancellable() @@ -267,22 +275,14 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { let parameters = Paths.DownloadRemoteImageParameters(type: imageType, imageURL: url) let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) - let response = try await userSession.client.send(imageRequest) - - let image = UIImage(data: response.data) + try await userSession.client.send(imageRequest) - guard let image else { - logger.error("Unable to download the the selected image") - throw JellyfinAPIError("An internal error occurred") - } - - try await uploadImage(image, index: index) + try await refreshItem() } // MARK: - Set Image From Local Files private func setLocalImage(_ url: URL, index: Int) async throws { - guard let itemID = item.id else { return } guard url.startAccessingSecurityScopedResource() else { return } defer { url.stopAccessingSecurityScopedResource() } @@ -290,7 +290,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { let image = UIImage(data: data) guard let image else { - logger.error("Unable to download the the selected image") + logger.error("Unable to access the the selected image") throw JellyfinAPIError("An internal error occurred") } @@ -302,7 +302,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { private func uploadImage(_ image: UIImage, index: Int) async throws { guard let itemID = item.id else { return } - let contentType: String + var contentType: String let imageData: Data if let pngData = image.pngData()?.base64EncodedData() { diff --git a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift index 97a562019..3ee60d87a 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift @@ -15,9 +15,13 @@ import SwiftUI struct EditItemImagesView: View { + // MARK: - Defaults + @Default(.accentColor) private var accentColor + // MARK: - Observed & Environment Objects + @EnvironmentObject private var router: ItemEditorCoordinator.Router @@ -43,7 +47,7 @@ struct EditItemImagesView: View { @ViewBuilder var body: some View { contentView - .navigationBarTitle("Images") + .navigationBarTitle(L10n.replaceImages) .navigationBarTitleDisplayMode(.inline) } diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index 3cadcfbac..24d25453e 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -98,7 +98,7 @@ struct ItemEditorView: View { @ViewBuilder private var editView: some View { Section(L10n.edit) { - ChevronButton("Images") + ChevronButton(L10n.replaceImages) .onSelect { router.route(to: \.editImages, viewModel.item) } diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift index 226a1da91..ad1e2d158 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift @@ -30,8 +30,6 @@ struct ItemImagePickerView: View { // MARK: - Dialog States - @State - private var isPresentingConfirmation: Bool = false @State private var isPresentingDeletion: Bool = false @State @@ -42,11 +40,7 @@ struct ItemImagePickerView: View { // MARK: - Selected Image @State - private var selectedImage: RemoteImageInfo? { - didSet { - isPresentingConfirmation = selectedImage != nil - } - } + private var selectedImage: RemoteImageInfo? // MARK: - Collection Layout @@ -68,33 +62,43 @@ struct ItemImagePickerView: View { } } .navigationBarMenuButton( - isLoading: viewModel.backgroundStates.contains { - $0 == .refreshing || $0 == .updating - } + isLoading: viewModel.backgroundStates.contains(.refreshing) ) { - menuContent() + Button(L10n.add, systemImage: "plus") { + isImportingFile = true + } + Divider() + Button(L10n.delete, systemImage: "trash", role: .destructive) { + isPresentingDeletion = true + } } .onReceive(viewModel.events) { event in - handleEvent(event) + switch event { + case .updated: + UIDevice.feedback(.success) + router.dismissCoordinator() + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + } } + .errorMessage($error) .fileImporter( isPresented: $isImportingFile, allowedContentTypes: [.image], allowsMultipleSelection: false ) { handleFileImport($0) } - .confirmationDialog( - L10n.save, - isPresented: $isPresentingConfirmation, - titleVisibility: .visible - ) { - confirmationDialogContent() + .sheet(item: $selectedImage, onDismiss: { + selectedImage = nil + }) { selectedImage in + confirmationSheet(selectedImage) } .confirmationDialog( L10n.delete, isPresented: $isPresentingDeletion, titleVisibility: .visible ) { - deletionDialogContent() + deletionSheet() } } @@ -107,6 +111,8 @@ struct ItemImagePickerView: View { DelayedProgressView() case .content: gridView + case .updating: + updateView case let .error(error): ErrorView(error: error) .onRetry { @@ -138,6 +144,20 @@ struct ItemImagePickerView: 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) + } + } + // MARK: - Poster Image Button private func imageButton(_ image: RemoteImageInfo?) -> some View { @@ -146,7 +166,7 @@ struct ItemImagePickerView: View { } label: { posterImage( image, - posterStyle: image?.height ?? 0 > image?.width ?? 0 ? .portrait : .landscape + posterStyle: .landscape // image?.height ?? 0 > image?.width ?? 0 ? .portrait : .landscape ) } } @@ -157,62 +177,75 @@ struct ItemImagePickerView: View { _ posterImageInfo: RemoteImageInfo?, posterStyle: PosterDisplayType ) -> some View { - VStack { - ZStack { - Color.secondarySystemFill - .frame(maxWidth: .infinity, maxHeight: .infinity) - - ImageView(URL(string: posterImageInfo?.url ?? "")) - .placeholder { source in - if let blurHash = source.blurHash { - BlurHashView(blurHash: blurHash, size: .Square(length: 8)) - .scaledToFit() - } else { - Image(systemName: "circle") - } + ZStack { + Color.secondarySystemFill + .frame(maxWidth: .infinity, maxHeight: .infinity) + + ImageView(URL(string: posterImageInfo?.url ?? "")) + .placeholder { source in + if let blurHash = source.blurHash { + BlurHashView(blurHash: blurHash, size: .Square(length: 8)) + .scaledToFit() + } else { + Image(systemName: "circle") } - .failure { - VStack(spacing: 8) { - Image(systemName: "photo") - Text(L10n.none) - } + } + .failure { + VStack(spacing: 8) { + Image(systemName: "photo") + Text(L10n.none) } - .foregroundColor(.secondary) - .font(.headline) - } - .scaledToFit() - .posterStyle(posterStyle) - - if let imageWidth = posterImageInfo?.width, let imageHeight = posterImageInfo?.height { - Text("\(imageWidth) x \(imageHeight)") - .font(.body) - } - - Text(posterImageInfo?.providerName ?? .emptyDash) - .font(.caption) + } + .foregroundColor(.secondary) + .font(.headline) } - .foregroundStyle(Color.secondary) + .scaledToFit() + .posterStyle(posterStyle) } - // MARK: - Set Image Dialog + // MARK: - Set Image Confirmation @ViewBuilder - private func confirmationDialogContent() -> some View { - Button(L10n.confirm) { - if let newURL = selectedImage?.url { - viewModel.send(.setImage(url: newURL)) + private func confirmationSheet(_ image: RemoteImageInfo) -> some View { + NavigationView { + VStack { + posterImage( + image, + posterStyle: image.height ?? 0 > image.width ?? 0 ? .portrait : .landscape + ) + .scaledToFit() + + if let imageWidth = image.width, let imageHeight = image.height { + Text("\(imageWidth) x \(imageHeight)") + .font(.body) + } + + Text(image.providerName ?? .emptyDash) + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.horizontal) + .navigationTitle(L10n.replaceImages) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + selectedImage = nil + } + .topBarTrailing { + Button(L10n.save) { + if let newURL = image.url { + viewModel.send(.setImage(url: newURL)) + } + selectedImage = nil + } + .buttonStyle(.toolbarPill) } - selectedImage = nil - } - Button(L10n.cancel, role: .cancel) { - selectedImage = nil } } - // MARK: - Delete Image Dialog + // MARK: - Delete Image Confirmation @ViewBuilder - private func deletionDialogContent() -> some View { + private func deletionSheet() -> some View { Button(L10n.delete, role: .destructive) { viewModel.send(.deleteImage) isPresentingDeletion = false @@ -223,19 +256,6 @@ struct ItemImagePickerView: View { } } - // MARK: - Handle ViewModel Events - - private func handleEvent(_ event: RemoteItemImageViewModel.Event) { - switch event { - case .updated: - UIDevice.feedback(.success) - router.dismissCoordinator() - case let .error(eventError): - UIDevice.feedback(.error) - error = eventError - } - } - // MARK: - Handle File Importing private func handleFileImport(_ result: Result<[URL], Error>) { @@ -248,18 +268,4 @@ struct ItemImagePickerView: View { error = fileError } } - - // MARK: - Navigation Menu Content - - private func menuContent() -> some View { - Group { - Button(L10n.add, systemImage: "plus") { - isImportingFile = true - } - Divider() - Button(L10n.delete, systemImage: "trash", role: .destructive) { - isPresentingDeletion = true - } - } - } } From da3a8edc71aebd0375a2eea56556fc2fb1022a11 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 21 Dec 2024 01:46:57 -0700 Subject: [PATCH 07/45] Lots of changes: Selecting a Remote image is now working without error and works consistently! Upload a local file is still broken Item types with multiple images is working as intended now! Overriding an image on index doesn't seem to work but it doesn't work for Web either so........ UI is way more jank but the hard parts are getting solved! --- .../Coordinators/ItemEditorCoordinator.swift | 9 +- .../ItemViewModel/ItemViewModel.swift | 55 +++++++++ .../EditItemImagesView.swift | 105 ++++++++++++++++-- .../ItemImagePickerView.swift | 32 ++---- 4 files changed, 164 insertions(+), 37 deletions(-) diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index f56725db7..6a36ac45a 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -28,7 +28,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @Route(.push) var editImages = makeEditImages - @Route(.modal) + @Route(.push) var imagePicker = makeImagePicker // MARK: - Route to Genres @@ -80,11 +80,8 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { EditItemImagesView(viewModel: ItemViewModel(item: item)) } - func makeImagePicker(viewModel: RemoteItemImageViewModel) - -> NavigationViewCoordinator { - NavigationViewCoordinator { - ItemImagePickerView(viewModel: viewModel) - } + func makeImagePicker(viewModel: RemoteItemImageViewModel) -> some View { + ItemImagePickerView(viewModel: viewModel) } // MARK: - Item Genres diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index cebab7706..e35c9e239 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -25,6 +25,7 @@ class ItemViewModel: ViewModel, Stateful { case replace(BaseItemDto) case toggleIsFavorite case toggleIsPlayed + case fetchAllImages } // MARK: BackgroundState @@ -70,6 +71,9 @@ class ItemViewModel: ViewModel, Stateful { @Published private(set) var specialFeatures: [BaseItemDto] = [] + @Published + private(set) var imagesByType: [String: [UIImage]] = [:] + @Published final var backgroundStates: OrderedSet = [] @Published @@ -263,12 +267,63 @@ class ItemViewModel: ViewModel, Stateful { } .asAnyCancellable() + return state + case .fetchAllImages: + + Task { [weak self] in + guard let self else { return } + do { + let imagesByType = try await self.getAllImages() + + await MainActor.run { + self.imagesByType = imagesByType + } + } catch { + await MainActor.run { + self.send(.error(.init(error.localizedDescription))) + } + } + } + .store(in: &cancellables) + return state } } func onRefresh() async throws {} + private func getAllImages() async throws -> [String: [UIImage]] { + guard let itemID = item.id else { + logger.error("Item ID not found") + return [:] + } + + var imagesByType: [String: [UIImage]] = [:] + + for imageType in ImageType.allCases { + var images: [UIImage] = [] + + var index = 0 + while true { + do { + let parameters = Paths.GetItemImageParameters(imageIndex: index) + let request = Paths.getItemImage(itemID: itemID, imageType: imageType.rawValue, parameters: parameters) + let response = try await userSession.client.send(request) + + if let image = UIImage(data: response.value) { + images.append(image) + } + + index += 1 + } catch { + break + } + } + imagesByType[imageType.rawValue] = images + } + return imagesByType + } + private func getFullItem() async throws -> BaseItemDto { var parameters = Paths.GetItemsByUserIDParameters() diff --git a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift index 3ee60d87a..097fb2890 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift @@ -49,37 +49,53 @@ struct EditItemImagesView: View { contentView .navigationBarTitle(L10n.replaceImages) .navigationBarTitleDisplayMode(.inline) + .onFirstAppear { + viewModel.send(.fetchAllImages) + } } // MARK: - Content View private var contentView: some View { ScrollView { - ForEach( - orderedItems, - id: \.self - ) { itemType in + ForEach(orderedItems, id: \.self) { imageType in Section { - imageButton(itemType) + if let images = viewModel.imagesByType[imageType.rawValue] { + if images.count == 1 { + // Render the single image using the specified logic + singleImageButton(imageType, imageIndex: 0) + } else { + // Render multiple images in a horizontal scroll view + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(images.indices, id: \.self) { index in + multipleImageButton(imageType, imageIndex: index) + } + } + .padding(.horizontal, 16) + } + } + } else { + missingImageView(for: imageType) + .frame(height: 150) + } Divider() .padding(.vertical, 16) } header: { HStack(alignment: .center) { - Text(itemType.rawValue.localizedCapitalized) + Text(imageType.rawValue.localizedCapitalized) Spacer() } .font(.headline) + .padding(.horizontal, 16) } - .padding( - EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) - ) } } } - // MARK: - Image Button View + // MARK: - Single Image Button View - private func imageButton(_ imageType: ImageType) -> some View { + private func singleImageButton(_ imageType: ImageType, imageIndex: Int) -> some View { Button { router.route( to: \.imagePicker, @@ -132,6 +148,73 @@ struct EditItemImagesView: View { } .scaledToFit() .posterStyle(.landscape) + .frame(width: 150, height: 150) + .cornerRadius(8) + .shadow(radius: 4) + .padding(.horizontal, 16) + } + } + + // MARK: - Multiple Image Button View + + private func multipleImageButton(_ imageType: ImageType, imageIndex: Int) -> some View { + Button { + router.route( + to: \.imagePicker, + RemoteItemImageViewModel( + item: viewModel.item, + imageType: imageType, + includeAllLanguages: false, + imageIndex: imageIndex + ) + ) + } label: { + ZStack(alignment: .bottomTrailing) { + if let image = viewModel.imagesByType[imageType.rawValue]?[imageIndex] { + Image(uiImage: image) + .resizable() + .scaledToFit() + .frame(width: 150, height: 150) + .cornerRadius(8) + .shadow(radius: 4) + } else { + Color.secondarySystemFill + .frame(width: 150, height: 150) + .cornerRadius(8) + .shadow(radius: 4) + } + + VStack { + Spacer() + HStack { + Spacer() + Image(systemName: "pencil.circle.fill") + .resizable() + .frame(width: 30, height: 30) + .shadow(radius: 10) + .symbolRenderingMode(.palette) + .foregroundStyle(accentColor.overlayColor, accentColor) + .padding(8) + } + } + } + } + } + + // MARK: - Missing Image View + + private func missingImageView(for imageType: ImageType) -> some View { + VStack { + Image(systemName: "photo") + .resizable() + .scaledToFit() + .frame(width: 100, height: 100) + .foregroundColor(.secondary) + Text(L10n.none) + .font(.subheadline) + .foregroundColor(.secondary) } + .frame(maxWidth: .infinity) + .padding() } } diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift index ad1e2d158..63754c80f 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift @@ -21,7 +21,7 @@ struct ItemImagePickerView: View { private var accentColor @EnvironmentObject - private var router: BasicNavigationViewCoordinator.Router + private var router: ItemEditorCoordinator.Router // MARK: - ViewModel @@ -51,11 +51,8 @@ struct ItemImagePickerView: View { var body: some View { contentView - .navigationBarTitle(viewModel.imageType.rawValue) + .navigationBarTitle(viewModel.imageType.rawValue.localizedCapitalized) .navigationBarTitleDisplayMode(.inline) - .navigationBarCloseButton { - router.dismissCoordinator() - } .onFirstAppear { if viewModel.state == .initial { viewModel.send(.refresh) @@ -87,7 +84,16 @@ struct ItemImagePickerView: View { isPresented: $isImportingFile, allowedContentTypes: [.image], allowsMultipleSelection: false - ) { handleFileImport($0) } + ) { + switch $0 { + case let .success(urls): + if let url = urls.first { + viewModel.send(.setLocalImage(url: url)) + } + case let .failure(fileError): + error = fileError + } + } .sheet(item: $selectedImage, onDismiss: { selectedImage = nil }) { selectedImage in @@ -249,23 +255,9 @@ struct ItemImagePickerView: View { Button(L10n.delete, role: .destructive) { viewModel.send(.deleteImage) isPresentingDeletion = false - router.dismissCoordinator() } Button(L10n.cancel, role: .cancel) { isPresentingDeletion = false } } - - // MARK: - Handle File Importing - - private func handleFileImport(_ result: Result<[URL], Error>) { - switch result { - case let .success(urls): - if let url = urls.first { - viewModel.send(.setLocalImage(url: url)) - } - case let .failure(fileError): - error = fileError - } - } } From 33f38bd7ac1dc84157b33dbfd0ffbb41b440b249 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 21 Dec 2024 20:34:17 -0700 Subject: [PATCH 08/45] Breaking this even more with the hopes of a better tomorrow. --- Shared/Components/LocalImageInfo.swift | 17 ++ .../Coordinators/ItemEditorCoordinator.swift | 8 +- ...wModel.swift => ItemImagesViewModel.swift} | 203 ++++++++----- .../ItemViewModel/ItemViewModel.swift | 55 ---- Swiftfin.xcodeproj/project.pbxproj | 34 ++- .../AddItemImageView.swift} | 82 ++---- .../EditItemImagesView.swift | 274 ++++++++---------- 7 files changed, 326 insertions(+), 347 deletions(-) create mode 100644 Shared/Components/LocalImageInfo.swift rename Shared/ViewModels/ItemAdministration/{RemoteItemImageViewModel.swift => ItemImagesViewModel.swift} (66%) rename Swiftfin/Views/ItemEditorView/{ItemImagePickerView/ItemImagePickerView.swift => AddItemImageView/AddItemImageView.swift} (73%) diff --git a/Shared/Components/LocalImageInfo.swift b/Shared/Components/LocalImageInfo.swift new file mode 100644 index 000000000..c76612f5a --- /dev/null +++ b/Shared/Components/LocalImageInfo.swift @@ -0,0 +1,17 @@ +// +// 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 + +struct LocalImageInfo: Identifiable { + let id = UUID() + var index: Int? + var image: UIImage? + var type: ImageType? +} diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 6a36ac45a..e0414e2fc 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -29,7 +29,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @Route(.push) var editImages = makeEditImages @Route(.push) - var imagePicker = makeImagePicker + var addImage = makeAddImage // MARK: - Route to Genres @@ -77,11 +77,11 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @ViewBuilder func makeEditImages(item: BaseItemDto) -> some View { - EditItemImagesView(viewModel: ItemViewModel(item: item)) + EditItemImagesView(viewModel: ItemImagesViewModel(item: item)) } - func makeImagePicker(viewModel: RemoteItemImageViewModel) -> some View { - ItemImagePickerView(viewModel: viewModel) + func makeAddImage(viewModel: ItemImagesViewModel) -> some View { + AddItemImageView(viewModel: viewModel) } // MARK: - Item Genres diff --git a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift similarity index 66% rename from Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift rename to Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index e479d31ab..187fbafe8 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteItemImageViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -17,7 +17,7 @@ import URLQueryEncoder private let DefaultPageSize = 50 -class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { +class ItemImagesViewModel: ViewModel, Stateful, Eventful { enum Event: Equatable { case updated @@ -27,10 +27,12 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { enum Action: Equatable { case cancel case refresh + case setImageType(ImageType?) + case getImages case getNextPage - case setImage(url: String) - case setLocalImage(url: URL) - case deleteImage + case setImage(url: String, index: Int = 0) + case uploadImage(image: UIImage, index: Int = 0) + case deleteImage(index: Int = 0) } enum BackgroundState: Hashable { @@ -45,29 +47,37 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { case error(JellyfinAPIError) } - @Published - var state: State = .initial - @Published - var backgroundStates: OrderedSet = [] + // MARK: - Published Variables @Published var item: BaseItemDto @Published - var imageType: ImageType + var includeAllLanguages: Bool @Published - var imageIndex: Int + var localImages: [String: [UIImage]] = [:] @Published - var includeAllLanguages: Bool + var remoteImages: IdentifiedArrayOf = [] @Published - var images: IdentifiedArrayOf = [] + var imageType: ImageType? + + // MARK: - Page Management private let pageSize: Int private(set) var currentPage: Int = 0 private(set) var hasNextPage: Bool = true + // MARK: - State Management + + @Published + var state: State = .initial + @Published + var backgroundStates: OrderedSet = [] + private var task: AnyCancellable? private let eventSubject = PassthroughSubject() + // MARK: - Eventful + var events: AnyPublisher { eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher() } @@ -76,15 +86,11 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { init( item: BaseItemDto, - imageType: ImageType, includeAllLanguages: Bool = false, - imageIndex: Int = 0, pageSize: Int = DefaultPageSize ) { self.item = item - self.imageType = imageType self.includeAllLanguages = includeAllLanguages - self.imageIndex = imageIndex self.pageSize = pageSize super.init() } @@ -100,7 +106,16 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { return state - case .refresh: + case let .setImageType(type): + self.imageType = type + return state + + case .getImages: + guard let imageType = imageType else { + logger.error("Image type not set") + return .error(JellyfinAPIError("Image type not set")) + } + task?.cancel() task = Task { [weak self] in @@ -108,13 +123,13 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { do { await MainActor.run { self.state = .initial - self.images.removeAll() + self.localImages.removeAll() self.currentPage = 0 self.hasNextPage = true _ = self.backgroundStates.append(.refreshing) } - try await self.getNextPage() + try await self.getNextPage(imageType) await MainActor.run { self.state = .content @@ -133,6 +148,11 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { return state case .getNextPage: + guard let imageType else { + logger.error("Image type not set") + return .error(JellyfinAPIError("Image type not set")) + } + guard hasNextPage else { return .content } task?.cancel() task = Task { [weak self] in @@ -142,7 +162,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = self.backgroundStates.append(.gettingNextPage) } - try await self.getNextPage() + try await self.getNextPage(imageType) await MainActor.run { self.state = .content @@ -160,7 +180,12 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { return state - case let .setImage(url): + case let .setImage(url, index): + guard let imageType else { + logger.error("Image type not set") + return .error(JellyfinAPIError("Image type not set")) + } + task?.cancel() task = Task { [weak self] in @@ -170,7 +195,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = self.state = .updating } - try await self.setImage(url, index: self.imageIndex) + try await self.setImage(url, type: imageType) await MainActor.run { self.eventSubject.send(.updated) @@ -187,7 +212,12 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { return state - case let .setLocalImage(url): + case let .uploadImage(image, index): + guard let imageType else { + logger.error("Image type not set") + return .error(JellyfinAPIError("Image type not set")) + } + task?.cancel() task = Task { [weak self] in @@ -197,7 +227,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = self.state = .updating } - try await self.setLocalImage(url, index: self.imageIndex) + try await self.uploadImage(image, type: imageType, index: index) await MainActor.run { self.eventSubject.send(.updated) @@ -214,7 +244,12 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { return state - case .deleteImage: + case let .deleteImage(index): + guard let imageType else { + logger.error("Image type not set") + return .error(JellyfinAPIError("Image type not set")) + } + task?.cancel() task = Task { [weak self] in @@ -224,7 +259,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { _ = self.state = .updating } - try await self.deleteImage(index: self.imageIndex) + try await self.deleteImage(imageType, index: index) await MainActor.run { self.eventSubject.send(.updated) @@ -240,17 +275,70 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { }.asAnyCancellable() return state + + case .refresh: + Task { [weak self] in + guard let self else { return } + do { + let localImages = try await self.getAllImages() + + await MainActor.run { + self.localImages = localImages + } + } catch { + await MainActor.run { + // self.send(.error(.init(error.localizedDescription))) + } + } + } + .store(in: &cancellables) + + return state + } + } + + // MARK: - Get All Images by Type + + private func getAllImages() async throws -> [String: [UIImage]] { + guard let itemID = item.id else { + logger.error("Item ID not found") + return [:] + } + + var imagesByType: [String: [UIImage]] = [:] + + for imageType in ImageType.allCases { + var images: [UIImage] = [] + + var index = 0 + while true { + do { + let parameters = Paths.GetItemImageParameters(imageIndex: index) + let request = Paths.getItemImage(itemID: itemID, imageType: imageType.rawValue, parameters: parameters) + let response = try await userSession.client.send(request) + + if let image = UIImage(data: response.value) { + images.append(image) + } + + index += 1 + } catch { + break + } + } + imagesByType[imageType.rawValue] = images } + return imagesByType } // MARK: - Paging Logic - private func getNextPage() async throws { + private func getNextPage(_ type: ImageType) async throws { guard let itemID = item.id, hasNextPage else { return } let startIndex = currentPage * pageSize let parameters = Paths.GetRemoteImagesParameters( - type: imageType, + type: type, startIndex: startIndex, limit: pageSize, isIncludeAllLanguages: includeAllLanguages @@ -258,48 +346,31 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { let request = Paths.getRemoteImages(itemID: itemID, parameters: parameters) let response = try await userSession.client.send(request) - let fetchedImages = response.value.images ?? [] + let newImages = response.value.images ?? [] - hasNextPage = fetchedImages.count >= pageSize + hasNextPage = newImages.count >= pageSize await MainActor.run { - images.append(contentsOf: fetchedImages) + remoteImages.append(contentsOf: newImages) currentPage += 1 } } // MARK: - Set Image From URL - private func setImage(_ url: String, index: Int) async throws { + private func setImage(_ url: String, type: ImageType) async throws { guard let itemID = item.id else { return } - let parameters = Paths.DownloadRemoteImageParameters(type: imageType, imageURL: url) + let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: url) let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) try await userSession.client.send(imageRequest) try await refreshItem() } - // MARK: - Set Image From Local Files - - private func setLocalImage(_ url: URL, index: Int) async throws { - guard url.startAccessingSecurityScopedResource() else { return } - defer { url.stopAccessingSecurityScopedResource() } - - let data = try Data(contentsOf: url) - let image = UIImage(data: data) - - guard let image else { - logger.error("Unable to access the the selected image") - throw JellyfinAPIError("An internal error occurred") - } - - try await uploadImage(image, index: index) - } - // MARK: - Upload Image - private func uploadImage(_ image: UIImage, index: Int) async throws { + private func uploadImage(_ image: UIImage, type: ImageType, index: Int = 0) async throws { guard let itemID = item.id else { return } var contentType: String @@ -318,7 +389,7 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { var request = Paths.setItemImageByIndex( itemID: itemID, - imageType: imageType.rawValue, + imageType: type.rawValue, imageIndex: index, imageData ) @@ -331,26 +402,24 @@ class RemoteItemImageViewModel: ViewModel, Stateful, Eventful { // MARK: - Delete Image - private func deleteImage(index: Int?) async throws { + private func deleteImage(_ type: ImageType, index: Int = 0) async throws { guard let itemID = item.id else { return } - var request: Request - - if let index { - request = Paths.deleteItemImageByIndex( - itemID: itemID, - imageType: imageType.rawValue, - imageIndex: index - ) - } else { - request = Paths.deleteItemImage( - itemID: itemID, - imageType: imageType.rawValue - ) - } + let request = Paths.deleteItemImageByIndex( + itemID: itemID, + imageType: type.rawValue, + imageIndex: index + ) _ = try await userSession.client.send(request) + await MainActor.run { + if var images = localImages[type.rawValue], index < images.count { + images.remove(at: index) + localImages[type.rawValue] = images + } + } + try await refreshItem() } diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index e35c9e239..cebab7706 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -25,7 +25,6 @@ class ItemViewModel: ViewModel, Stateful { case replace(BaseItemDto) case toggleIsFavorite case toggleIsPlayed - case fetchAllImages } // MARK: BackgroundState @@ -71,9 +70,6 @@ class ItemViewModel: ViewModel, Stateful { @Published private(set) var specialFeatures: [BaseItemDto] = [] - @Published - private(set) var imagesByType: [String: [UIImage]] = [:] - @Published final var backgroundStates: OrderedSet = [] @Published @@ -267,63 +263,12 @@ class ItemViewModel: ViewModel, Stateful { } .asAnyCancellable() - return state - case .fetchAllImages: - - Task { [weak self] in - guard let self else { return } - do { - let imagesByType = try await self.getAllImages() - - await MainActor.run { - self.imagesByType = imagesByType - } - } catch { - await MainActor.run { - self.send(.error(.init(error.localizedDescription))) - } - } - } - .store(in: &cancellables) - return state } } func onRefresh() async throws {} - private func getAllImages() async throws -> [String: [UIImage]] { - guard let itemID = item.id else { - logger.error("Item ID not found") - return [:] - } - - var imagesByType: [String: [UIImage]] = [:] - - for imageType in ImageType.allCases { - var images: [UIImage] = [] - - var index = 0 - while true { - do { - let parameters = Paths.GetItemImageParameters(imageIndex: index) - let request = Paths.getItemImage(itemID: itemID, imageType: imageType.rawValue, parameters: parameters) - let response = try await userSession.client.send(request) - - if let image = UIImage(data: response.value) { - images.append(image) - } - - index += 1 - } catch { - break - } - } - imagesByType[imageType.rawValue] = images - } - return imagesByType - } - private func getFullItem() async throws -> BaseItemDto { var parameters = Paths.GetItemsByUserIDParameters() diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index fe2e2cca9..6369adfa8 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -69,12 +69,14 @@ 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 */; }; + 4E37F6132D17BB730022AADD /* LocalImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */; }; + 4E37F6142D17BB730022AADD /* LocalImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */; }; 4E3A24DA2CFE34A00083A72C /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */; }; 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; - 4E45939E2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */; }; - 4E45939F2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */; }; + 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; + 4E45939F2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; 4E4593A32D04E2B500E277E1 /* EditItemImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */; }; - 4E4593A62D04E4E300E277E1 /* ItemImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A52D04E4DE00E277E1 /* ItemImagePickerView.swift */; }; + 4E4593A62D04E4E300E277E1 /* AddItemImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */; }; 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */; }; 4E49DECF2CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */; }; @@ -768,7 +770,6 @@ E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A282BF3046A004DF6AB /* AddUserButton.swift */; }; E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; }; E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */; }; - E1763A662BF3CA83004DF6AB /* FullScreenMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */; }; E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserButton.swift */; }; E1763A712BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; }; E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; }; @@ -1225,11 +1226,12 @@ 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 = ""; }; + 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImageInfo.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 = ""; }; - 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemImageViewModel.swift; sourceTree = ""; }; + 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesViewModel.swift; sourceTree = ""; }; 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemImagesView.swift; sourceTree = ""; }; - 4E4593A52D04E4DE00E277E1 /* ItemImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePickerView.swift; sourceTree = ""; }; + 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemImageView.swift; sourceTree = ""; }; 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = ""; }; 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionSection.swift; sourceTree = ""; }; 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxBitratePolicy.swift; sourceTree = ""; }; @@ -1705,7 +1707,6 @@ E1763A282BF3046A004DF6AB /* AddUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = ""; }; E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGridButton.swift; sourceTree = ""; }; E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowButton.swift; sourceTree = ""; }; - E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMenu.swift; sourceTree = ""; }; E1763A692BF3D177004DF6AB /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = ""; }; E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+Mappings.swift"; sourceTree = ""; }; E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = ""; }; @@ -2266,12 +2267,12 @@ path = EditItemImagesView; sourceTree = ""; }; - 4E4593A42D04E4D600E277E1 /* ItemImagePickerView */ = { + 4E4593A42D04E4D600E277E1 /* AddItemImageView */ = { isa = PBXGroup; children = ( - 4E4593A52D04E4DE00E277E1 /* ItemImagePickerView.swift */, + 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */, ); - path = ItemImagePickerView; + path = AddItemImageView; sourceTree = ""; }; 4E49DEDE2CE55F7F00352DCD /* Components */ = { @@ -2497,7 +2498,7 @@ 4E4593A12D04E2A200E277E1 /* EditItemImagesView */, 4E6619FF2CEFE39000025C99 /* EditMetadataView */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, - 4E4593A42D04E4D600E277E1 /* ItemImagePickerView */, + 4E4593A42D04E4D600E277E1 /* AddItemImageView */, ); path = ItemEditorView; sourceTree = ""; @@ -2516,7 +2517,7 @@ 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, - 4E45939D2D04E1E600E277E1 /* RemoteItemImageViewModel.swift */, + 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */, ); path = ItemAdministration; sourceTree = ""; @@ -4417,6 +4418,7 @@ 531AC8BE26750DE20091C7EB /* ImageView.swift */, 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */, 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */, + 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */, E1D37F472B9C648E00343D2B /* MaxHeightText.swift */, E1DC983F296DEBA500982F06 /* PosterIndicators */, E1FE69A628C29B720021BC93 /* ProgressBar.swift */, @@ -5405,6 +5407,7 @@ 4E4E9C6B2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */, E1575E9B293E7B1E001665B1 /* EnvironmentValue+Keys.swift in Sources */, E133328929538D8D00EE76AB /* Files.swift in Sources */, + 4E37F6132D17BB730022AADD /* LocalImageInfo.swift in Sources */, E154967A296CB4B000C4EF88 /* VideoPlayerSettingsView.swift in Sources */, C46008742A97DFF2002B1C7A /* LiveLoadingOverlay.swift in Sources */, 4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */, @@ -5440,7 +5443,7 @@ 4E8F74B12CE03EB000CC8969 /* RefreshMetadataViewModel.swift in Sources */, E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */, E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */, - 4E45939F2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */, + 4E45939F2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */, E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */, @@ -5704,7 +5707,7 @@ E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */, 4E8F74B22CE03EB000CC8969 /* RefreshMetadataViewModel.swift in Sources */, E129429B28F4A5E300796AC6 /* PlaybackSettingsView.swift in Sources */, - 4E4593A62D04E4E300E277E1 /* ItemImagePickerView.swift in Sources */, + 4E4593A62D04E4E300E277E1 /* AddItemImageView.swift in Sources */, E1E9017B28DAAE4D001B1594 /* RoundedCorner.swift in Sources */, E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */, 4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */, @@ -5810,7 +5813,7 @@ E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */, 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */, - 4E45939E2D04E20000E277E1 /* RemoteItemImageViewModel.swift in Sources */, + 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */, C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */, E18E01AD288746AF0022598C /* DotHStack.swift in Sources */, E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */, @@ -6091,6 +6094,7 @@ 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, E164A7F42BE4736300A54B18 /* SignOutIntervalSection.swift in Sources */, 4E8F74AF2CE03E2E00CC8969 /* RefreshMetadataButton.swift in Sources */, + 4E37F6142D17BB730022AADD /* LocalImageInfo.swift in Sources */, E148128528C15472003B8787 /* SortOrder+ItemSortOrder.swift in Sources */, E10231602BCF8B7E009D71FC /* VideoPlayerWrapperCoordinator.swift in Sources */, 4E4E9C6A2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift b/Swiftfin/Views/ItemEditorView/AddItemImageView/AddItemImageView.swift similarity index 73% rename from Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift rename to Swiftfin/Views/ItemEditorView/AddItemImageView/AddItemImageView.swift index 63754c80f..c96566a93 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImagePickerView/ItemImagePickerView.swift +++ b/Swiftfin/Views/ItemEditorView/AddItemImageView/AddItemImageView.swift @@ -13,27 +13,23 @@ import Defaults import JellyfinAPI import SwiftUI -struct ItemImagePickerView: View { +struct AddItemImageView: View { - // MARK: - Defaults and Environment + // MARK: - Defaults @Default(.accentColor) private var accentColor + // MARK: - State & Environment Objects + @EnvironmentObject private var router: ItemEditorCoordinator.Router - // MARK: - ViewModel - - @ObservedObject - var viewModel: RemoteItemImageViewModel + @StateObject + var viewModel: ItemImagesViewModel // MARK: - Dialog States - @State - private var isPresentingDeletion: Bool = false - @State - private var isImportingFile: Bool = false @State private var error: Error? @@ -49,24 +45,24 @@ struct ItemImagePickerView: View { // MARK: - Body + init(viewModel: ItemImagesViewModel) { + self._viewModel = StateObject(wrappedValue: viewModel) + } + + // MARK: - Body + var body: some View { contentView - .navigationBarTitle(viewModel.imageType.rawValue.localizedCapitalized) + .navigationBarTitle(viewModel.imageType?.rawValue.localizedCapitalized ?? L10n.unknown) .navigationBarTitleDisplayMode(.inline) - .onFirstAppear { - if viewModel.state == .initial { - viewModel.send(.refresh) + .topBarTrailing { + if viewModel.backgroundStates.contains(.refreshing) { + ProgramsView() } } - .navigationBarMenuButton( - isLoading: viewModel.backgroundStates.contains(.refreshing) - ) { - Button(L10n.add, systemImage: "plus") { - isImportingFile = true - } - Divider() - Button(L10n.delete, systemImage: "trash", role: .destructive) { - isPresentingDeletion = true + .onFirstAppear { + if viewModel.state == .initial { + viewModel.send(.getImages) } } .onReceive(viewModel.events) { event in @@ -80,32 +76,11 @@ struct ItemImagePickerView: View { } } .errorMessage($error) - .fileImporter( - isPresented: $isImportingFile, - allowedContentTypes: [.image], - allowsMultipleSelection: false - ) { - switch $0 { - case let .success(urls): - if let url = urls.first { - viewModel.send(.setLocalImage(url: url)) - } - case let .failure(fileError): - error = fileError - } - } .sheet(item: $selectedImage, onDismiss: { selectedImage = nil }) { selectedImage in confirmationSheet(selectedImage) } - .confirmationDialog( - L10n.delete, - isPresented: $isPresentingDeletion, - titleVisibility: .visible - ) { - deletionSheet() - } } // MARK: - Content View @@ -122,7 +97,7 @@ struct ItemImagePickerView: View { case let .error(error): ErrorView(error: error) .onRetry { - viewModel.send(.refresh) + viewModel.send(.getImages) } } } @@ -131,14 +106,14 @@ struct ItemImagePickerView: View { @ViewBuilder private var gridView: some View { - if viewModel.images.isEmpty { + if viewModel.remoteImages.isEmpty { Text(L10n.none) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .listRowSeparator(.hidden) .listRowInsets(.zero) } else { CollectionVGrid( - uniqueElements: viewModel.images, + uniqueElements: viewModel.remoteImages, layout: layout ) { image in imageButton(image) @@ -247,17 +222,4 @@ struct ItemImagePickerView: View { } } } - - // MARK: - Delete Image Confirmation - - @ViewBuilder - private func deletionSheet() -> some View { - Button(L10n.delete, role: .destructive) { - viewModel.send(.deleteImage) - isPresentingDeletion = false - } - Button(L10n.cancel, role: .cancel) { - isPresentingDeletion = false - } - } } diff --git a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift index 097fb2890..8acf46781 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift @@ -26,31 +26,53 @@ struct EditItemImagesView: View { private var router: ItemEditorCoordinator.Router @ObservedObject - var viewModel: ItemViewModel + var viewModel: ItemImagesViewModel - // MARK: - Ordered Items + // MARK: - Dialog State + + @State + private var isImportingImage = false + @State + private var isDeletingImage = false + + @State + private var selectedImage: LocalImageInfo? + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Computed Properties private var orderedItems: [ImageType] { - ImageType.allCases.sorted { (lhs: ImageType, rhs: ImageType) in - if lhs == .primary && rhs != .primary { - return true - } else if lhs != .primary && rhs == .primary { - return false - } else { - return lhs.rawValue.localizedCaseInsensitiveCompare(rhs.rawValue) == .orderedAscending - } + ImageType.allCases.sorted { lhs, rhs in + if lhs == .primary { return true } + if rhs == .primary { return false } + return lhs.rawValue.localizedCaseInsensitiveCompare(rhs.rawValue) == .orderedAscending } } // MARK: - Body - @ViewBuilder var body: some View { contentView .navigationBarTitle(L10n.replaceImages) .navigationBarTitleDisplayMode(.inline) - .onFirstAppear { - viewModel.send(.fetchAllImages) + .onAppear { viewModel.send(.refresh) } + .sheet(item: $selectedImage) { item in + deletionSheet( + item.image!, + type: item.type!, + index: item.index ?? 0 + ) + } + .fileImporter( + isPresented: $isImportingImage, + allowedContentTypes: [.image], + allowsMultipleSelection: false + ) { + handleFileImport(result: $0) } } @@ -60,161 +82,121 @@ struct EditItemImagesView: View { ScrollView { ForEach(orderedItems, id: \.self) { imageType in Section { - if let images = viewModel.imagesByType[imageType.rawValue] { - if images.count == 1 { - // Render the single image using the specified logic - singleImageButton(imageType, imageIndex: 0) - } else { - // Render multiple images in a horizontal scroll view - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { - ForEach(images.indices, id: \.self) { index in - multipleImageButton(imageType, imageIndex: index) - } - } - .padding(.horizontal, 16) - } - } - } else { - missingImageView(for: imageType) - .frame(height: 150) - } - Divider() - .padding(.vertical, 16) + imageScrollView(for: imageType) + Divider().padding(.vertical, 16) } header: { - HStack(alignment: .center) { - Text(imageType.rawValue.localizedCapitalized) - Spacer() - } - .font(.headline) - .padding(.horizontal, 16) + sectionHeader(for: imageType) } } } } - // MARK: - Single Image Button View + // MARK: - Helpers - private func singleImageButton(_ imageType: ImageType, imageIndex: Int) -> some View { - Button { - router.route( - to: \.imagePicker, - RemoteItemImageViewModel( - item: viewModel.item, - imageType: imageType, - includeAllLanguages: false - ) - ) - } label: { - ZStack(alignment: .bottomTrailing) { - Color.secondarySystemFill - .frame(maxWidth: .infinity, maxHeight: .infinity) - - ZStack { - RedrawOnNotificationView(.itemMetadataDidChange) { - ImageView(viewModel.item.imageSource(imageType)) - .placeholder { source in - if let blurHash = source.blurHash { - BlurHashView(blurHash: blurHash, size: .Square(length: 8)) - .scaledToFit() - } else { - Image(systemName: "circle") - } - } - .failure { - VStack(spacing: 8) { - Image(systemName: "photo") - Text(L10n.none) - } - } - .foregroundColor(.secondary) - .font(.headline) - } - - VStack { - Spacer() - HStack { - Spacer() - Image(systemName: "pencil.circle.fill") - .resizable() - .frame(width: 30, height: 30) - .shadow(radius: 10) - .symbolRenderingMode(.palette) - .foregroundStyle(accentColor.overlayColor, accentColor) - .padding(8) + @ViewBuilder + private func imageScrollView(for imageType: ImageType) -> some View { + if let images = viewModel.localImages[imageType.rawValue] { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(images, id: \.self) { image in + imageButton(image) { + selectedImage = LocalImageInfo( + index: 0, + image: image, + type: imageType + ) } } } + .padding(.horizontal, 16) + } + } + } + + @ViewBuilder + private func sectionHeader(for imageType: ImageType) -> some View { + HStack(alignment: .center, spacing: 16) { + Text(imageType.rawValue.localizedCapitalized) + Spacer() + Button(action: { viewModel.send(.setImageType(imageType)) + router.route(to: \.addImage, viewModel) + }) { + Image(systemName: "magnifyingglass") + } + Button(action: { isImportingImage = true }) { + Image(systemName: "plus") + } + } + .font(.headline) + .padding(.horizontal, 16) + } + + private func imageButton(_ image: UIImage, onSelect: @escaping () -> Void) -> some View { + Button(action: onSelect) { + ZStack { + Color.secondarySystemFill + Image(uiImage: image) + .resizable() + .scaledToFit() } - .scaledToFit() .posterStyle(.landscape) - .frame(width: 150, height: 150) - .cornerRadius(8) + .frame(maxHeight: 150) .shadow(radius: 4) - .padding(.horizontal, 16) + .padding(16) } } - // MARK: - Multiple Image Button View - - private func multipleImageButton(_ imageType: ImageType, imageIndex: Int) -> some View { - Button { - router.route( - to: \.imagePicker, - RemoteItemImageViewModel( - item: viewModel.item, - imageType: imageType, - includeAllLanguages: false, - imageIndex: imageIndex - ) - ) - } label: { - ZStack(alignment: .bottomTrailing) { - if let image = viewModel.imagesByType[imageType.rawValue]?[imageIndex] { - Image(uiImage: image) - .resizable() - .scaledToFit() - .frame(width: 150, height: 150) - .cornerRadius(8) - .shadow(radius: 4) - } else { - Color.secondarySystemFill - .frame(width: 150, height: 150) - .cornerRadius(8) - .shadow(radius: 4) - } - - VStack { - Spacer() - HStack { - Spacer() - Image(systemName: "pencil.circle.fill") - .resizable() - .frame(width: 30, height: 30) - .shadow(radius: 10) - .symbolRenderingMode(.palette) - .foregroundStyle(accentColor.overlayColor, accentColor) - .padding(8) + private func handleFileImport(result: Result<[URL], Error>) { + switch result { + case let .success(urls): + if let url = urls.first { + do { + let data = try Data(contentsOf: url) + if let image = UIImage(data: data) { + viewModel.send(.uploadImage(image: image)) } + } catch { + print("Error loading image data: \(error.localizedDescription)") } } + case let .failure(fileError): + error = fileError + print("File import error: \(fileError.localizedDescription)") } } - // MARK: - Missing Image View - - private func missingImageView(for imageType: ImageType) -> some View { - VStack { - Image(systemName: "photo") - .resizable() - .scaledToFit() - .frame(width: 100, height: 100) - .foregroundColor(.secondary) - Text(L10n.none) - .font(.subheadline) - .foregroundColor(.secondary) + // MARK: - Delete Image Confirmation + + @ViewBuilder + private func deletionSheet(_ image: UIImage, type: ImageType, index: Int) -> some View { + NavigationView { + VStack { + Image(uiImage: image) + .resizable() + .scaledToFit() + + Text("\(Int(image.size.width)) x \(Int(image.size.height))") + .font(.body) + + Text(image.accessibilityIdentifier ?? "-") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.horizontal) + .navigationTitle(L10n.replaceImages) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + selectedImage = nil + } + .topBarTrailing { + Button(L10n.delete, role: .destructive) { + viewModel.send(.setImageType(type)) + viewModel.send(.deleteImage(index: index)) + viewModel.send(.setImageType(nil)) + selectedImage = nil + } + .buttonStyle(.toolbarPill(.red)) + } } - .frame(maxWidth: .infinity) - .padding() } } From 08a81079c1a54128780fcd29fea9a9fa76eeb96a Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 22 Dec 2024 00:16:03 -0700 Subject: [PATCH 09/45] Getting better? --- Shared/Components/LocalImageInfo.swift | 17 -- .../Coordinators/ItemEditorCoordinator.swift | 2 +- .../ItemImagesViewModel.swift | 239 +++++------------- .../RemoteImageInfoViewModel.swift | 177 +++++++++++++ Swiftfin.xcodeproj/project.pbxproj | 40 ++- .../AddItemImageView/AddItemImageView.swift | 34 +-- .../EditItemImagesView.swift | 109 ++++---- .../AddItemElementView.swift | 0 .../Components/NameInput.swift | 0 .../Components/SearchResultsSection.swift | 0 .../Components/EditItemElementRow.swift | 0 .../EditItemElementView.swift | 0 .../Components/Sections/DateSection.swift | 0 .../Sections/DisplayOrderSection.swift | 0 .../Components/Sections/EpisodeSection.swift | 0 .../Sections/LocalizationSection.swift | 0 .../Sections/LockMetadataSection.swift | 0 .../Sections/MediaFormatSection.swift | 0 .../Components/Sections/OverviewSection.swift | 0 .../Sections/ParentialRatingsSection.swift | 0 .../Components/Sections/ReviewsSection.swift | 0 .../Components/Sections/SeriesSection.swift | 0 .../Components/Sections/TitleSection.swift | 0 .../EditMetadataView/EditMetadataView.swift | 0 24 files changed, 346 insertions(+), 272 deletions(-) delete mode 100644 Shared/Components/LocalImageInfo.swift create mode 100644 Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift rename Swiftfin/Views/ItemEditorView/{ => ItemImages}/AddItemImageView/AddItemImageView.swift (86%) rename Swiftfin/Views/ItemEditorView/{ => ItemImages}/EditItemImagesView/EditItemImagesView.swift (65%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/AddItemElementView/AddItemElementView.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/AddItemElementView/Components/NameInput.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/AddItemElementView/Components/SearchResultsSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditItemElementView/Components/EditItemElementRow.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditItemElementView/EditItemElementView.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/DateSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/DisplayOrderSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/EpisodeSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/LocalizationSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/LockMetadataSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/MediaFormatSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/OverviewSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/ParentialRatingsSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/ReviewsSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/SeriesSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/Components/Sections/TitleSection.swift (100%) rename Swiftfin/Views/ItemEditorView/{ => ItemMetadata}/EditMetadataView/EditMetadataView.swift (100%) diff --git a/Shared/Components/LocalImageInfo.swift b/Shared/Components/LocalImageInfo.swift deleted file mode 100644 index c76612f5a..000000000 --- a/Shared/Components/LocalImageInfo.swift +++ /dev/null @@ -1,17 +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 - -struct LocalImageInfo: Identifiable { - let id = UUID() - var index: Int? - var image: UIImage? - var type: ImageType? -} diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index e0414e2fc..98b382651 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -80,7 +80,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { EditItemImagesView(viewModel: ItemImagesViewModel(item: item)) } - func makeAddImage(viewModel: ItemImagesViewModel) -> some View { + func makeAddImage(viewModel: RemoteImageInfoViewModel) -> some View { AddItemImageView(viewModel: viewModel) } diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 187fbafe8..6d141f5ac 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -8,35 +8,27 @@ import Combine import Foundation -import Get import IdentifiedCollections import JellyfinAPI import OrderedCollections import UIKit -import URLQueryEncoder - -private let DefaultPageSize = 50 class ItemImagesViewModel: ViewModel, Stateful, Eventful { enum Event: Equatable { case updated + case deleted case error(JellyfinAPIError) } enum Action: Equatable { - case cancel case refresh - case setImageType(ImageType?) - case getImages - case getNextPage - case setImage(url: String, index: Int = 0) - case uploadImage(image: UIImage, index: Int = 0) - case deleteImage(index: Int = 0) + case setImage(url: String, type: ImageType) + case uploadImage(image: UIImage, type: ImageType, index: Int = 0) + case deleteImage(type: ImageType, index: Int = 0) } enum BackgroundState: Hashable { - case gettingNextPage case refreshing } @@ -44,27 +36,20 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { case initial case content case updating + case deleting case error(JellyfinAPIError) } + // MARK: - Image Variables + + private let includeAllLanguages: Bool + // MARK: - Published Variables @Published var item: BaseItemDto @Published - var includeAllLanguages: Bool - @Published - var localImages: [String: [UIImage]] = [:] - @Published - var remoteImages: IdentifiedArrayOf = [] - @Published - var imageType: ImageType? - - // MARK: - Page Management - - private let pageSize: Int - private(set) var currentPage: Int = 0 - private(set) var hasNextPage: Bool = true + var images: [String: [UIImage]] = [:] // MARK: - State Management @@ -86,12 +71,10 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { init( item: BaseItemDto, - includeAllLanguages: Bool = false, - pageSize: Int = DefaultPageSize + includeAllLanguages: Bool = false ) { self.item = item self.includeAllLanguages = includeAllLanguages - self.pageSize = pageSize super.init() } @@ -100,22 +83,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { func respond(to action: Action) -> State { switch action { - case .cancel: - task?.cancel() - self.state = .initial - - return state - - case let .setImageType(type): - self.imageType = type - return state - - case .getImages: - guard let imageType = imageType else { - logger.error("Image type not set") - return .error(JellyfinAPIError("Image type not set")) - } - + case .refresh: task?.cancel() task = Task { [weak self] in @@ -123,13 +91,11 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { do { await MainActor.run { self.state = .initial - self.localImages.removeAll() - self.currentPage = 0 - self.hasNextPage = true + self.images.removeAll() _ = self.backgroundStates.append(.refreshing) } - try await self.getNextPage(imageType) + try await self.getAllImages() await MainActor.run { self.state = .content @@ -147,45 +113,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case .getNextPage: - guard let imageType else { - logger.error("Image type not set") - return .error(JellyfinAPIError("Image type not set")) - } - - guard hasNextPage else { return .content } - task?.cancel() - task = Task { [weak self] in - guard let self else { return } - do { - await MainActor.run { - _ = self.backgroundStates.append(.gettingNextPage) - } - - try await self.getNextPage(imageType) - - await MainActor.run { - self.state = .content - _ = self.backgroundStates.remove(.gettingNextPage) - } - } catch { - let apiError = JellyfinAPIError(error.localizedDescription) - await MainActor.run { - self.state = .error(apiError) - self.eventSubject.send(.error(apiError)) - _ = self.backgroundStates.remove(.gettingNextPage) - } - } - }.asAnyCancellable() - - return state - - case let .setImage(url, index): - guard let imageType else { - logger.error("Image type not set") - return .error(JellyfinAPIError("Image type not set")) - } - + case let .setImage(url, imageType): task?.cancel() task = Task { [weak self] in @@ -212,12 +140,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .uploadImage(image, index): - guard let imageType else { - logger.error("Image type not set") - return .error(JellyfinAPIError("Image type not set")) - } - + case let .uploadImage(image, imageType, index): task?.cancel() task = Task { [weak self] in @@ -244,115 +167,75 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .deleteImage(index): - guard let imageType else { - logger.error("Image type not set") - return .error(JellyfinAPIError("Image type not set")) - } - + case let .deleteImage(imageType, index): task?.cancel() task = Task { [weak self] in guard let self = self else { return } do { await MainActor.run { - _ = self.state = .updating + _ = self.state = .deleting } try await self.deleteImage(imageType, index: index) await MainActor.run { - self.eventSubject.send(.updated) - _ = self.state = .updating + self.eventSubject.send(.deleted) + _ = self.state = .deleting } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { self.eventSubject.send(.error(apiError)) - _ = self.state = .updating + _ = self.state = .deleting } } }.asAnyCancellable() - return state - - case .refresh: - Task { [weak self] in - guard let self else { return } - do { - let localImages = try await self.getAllImages() - - await MainActor.run { - self.localImages = localImages - } - } catch { - await MainActor.run { - // self.send(.error(.init(error.localizedDescription))) - } - } - } - .store(in: &cancellables) - return state } } // MARK: - Get All Images by Type - private func getAllImages() async throws -> [String: [UIImage]] { - guard let itemID = item.id else { - logger.error("Item ID not found") - return [:] - } - - var imagesByType: [String: [UIImage]] = [:] - - for imageType in ImageType.allCases { - var images: [UIImage] = [] - - var index = 0 - while true { - do { - let parameters = Paths.GetItemImageParameters(imageIndex: index) - let request = Paths.getItemImage(itemID: itemID, imageType: imageType.rawValue, parameters: parameters) - let response = try await userSession.client.send(request) + private func getAllImages() async throws { + guard let itemID = item.id else { return } - if let image = UIImage(data: response.value) { - images.append(image) + let results = try await withThrowingTaskGroup(of: (String, [UIImage]).self) { group -> [String: [UIImage]] in + for imageType in ImageType.allCases { + group.addTask { + var images: [UIImage] = [] + var index = 0 + + while true { + do { + let parameters = Paths.GetItemImageParameters(imageIndex: index) + let request = Paths.getItemImage(itemID: itemID, imageType: imageType.rawValue, parameters: parameters) + let response = try await self.userSession.client.send(request) + + if let image = UIImage(data: response.value) { + images.append(image) + } + + index += 1 + } catch { + break + } } - index += 1 - } catch { - break + return (imageType.rawValue, images) } } - imagesByType[imageType.rawValue] = images - } - return imagesByType - } - - // MARK: - Paging Logic - private func getNextPage(_ type: ImageType) async throws { - guard let itemID = item.id, hasNextPage else { return } - - let startIndex = currentPage * pageSize - let parameters = Paths.GetRemoteImagesParameters( - type: type, - startIndex: startIndex, - limit: pageSize, - isIncludeAllLanguages: includeAllLanguages - ) - - let request = Paths.getRemoteImages(itemID: itemID, parameters: parameters) - let response = try await userSession.client.send(request) - let newImages = response.value.images ?? [] - - hasNextPage = newImages.count >= pageSize + var collectedResults: [String: [UIImage]] = [:] + for try await (key, images) in group { + collectedResults[key] = images + } + return collectedResults + } await MainActor.run { - remoteImages.append(contentsOf: newImages) - currentPage += 1 + self.images = results } } @@ -370,20 +253,21 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { // MARK: - Upload Image + // TODO: Make actually work. 500 error. Bad format. private func uploadImage(_ image: UIImage, type: ImageType, index: Int = 0) async throws { guard let itemID = item.id else { return } - var contentType: String + let contentType: String let imageData: Data - if let pngData = image.pngData()?.base64EncodedData() { + if let pngData = image.pngData() { contentType = "image/png" imageData = pngData - } else if let jpgData = image.jpegData(compressionQuality: 1)?.base64EncodedData() { + } else if let jpgData = image.jpegData(compressionQuality: 1) { contentType = "image/jpeg" imageData = jpgData } else { - logger.error("Unable to upload the the selected image") + logger.error("Unable to upload the selected image") throw JellyfinAPIError("An internal error occurred") } @@ -391,7 +275,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { itemID: itemID, imageType: type.rawValue, imageIndex: index, - imageData + imageData.base64EncodedData() ) request.headers = ["Content-Type": contentType] @@ -414,13 +298,12 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { _ = try await userSession.client.send(request) await MainActor.run { - if var images = localImages[type.rawValue], index < images.count { - images.remove(at: index) - localImages[type.rawValue] = images + if var typeImages = images[type.rawValue], index < typeImages.count { + typeImages.remove(at: index) + images[type.rawValue] = typeImages } + Notifications[.itemMetadataDidChange].post(item) } - - try await refreshItem() } // MARK: - Refresh Item diff --git a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift new file mode 100644 index 000000000..6cbb547ab --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift @@ -0,0 +1,177 @@ +// +// 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 IdentifiedCollections +import JellyfinAPI +import OrderedCollections +import UIKit +import URLQueryEncoder + +private let DefaultPageSize = 50 + +class RemoteImageInfoViewModel: ViewModel, Stateful { + + enum Action: Equatable { + case cancel + case refresh + case getNextPage + } + + enum BackgroundState: Hashable { + case gettingNextPage + case refreshing + } + + enum State: Hashable { + case initial + case content + case error(JellyfinAPIError) + } + + // MARK: - Published Variables + + @Published + var item: BaseItemDto + @Published + var imageType: ImageType + @Published + var includeAllLanguages: Bool + @Published + var images: IdentifiedArrayOf = [] + + // MARK: - Page Management + + private let pageSize: Int + private(set) var currentPage: Int = 0 + private(set) var hasNextPage: Bool = true + + // MARK: - State Management + + @Published + var state: State = .initial + @Published + var backgroundStates: OrderedSet = [] + + private var task: AnyCancellable? + + // MARK: - Initializer + + init( + item: BaseItemDto, + imageType: ImageType, + includeAllLanguages: Bool = false, + pageSize: Int = DefaultPageSize + ) { + self.item = item + self.imageType = imageType + self.includeAllLanguages = includeAllLanguages + self.pageSize = pageSize + super.init() + } + + // MARK: - Respond to Actions + + func respond(to action: Action) -> State { + switch action { + + case .cancel: + task?.cancel() + self.state = .initial + + return state + + case .refresh: + task?.cancel() + + task = Task { [weak self] in + guard let self else { return } + do { + await MainActor.run { + self.state = .initial + self.images.removeAll() + self.currentPage = 0 + self.hasNextPage = true + _ = self.backgroundStates.append(.refreshing) + } + + try await self.getNextPage(imageType) + + await MainActor.run { + self.state = .content + _ = self.backgroundStates.remove(.refreshing) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + _ = self.backgroundStates.remove(.refreshing) + } + } + }.asAnyCancellable() + + return state + + case .getNextPage: + guard hasNextPage else { return .content } + + task?.cancel() + + task = Task { [weak self] in + guard let self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.gettingNextPage) + } + + try await self.getNextPage(imageType) + + await MainActor.run { + self.state = .content + _ = self.backgroundStates.remove(.gettingNextPage) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + _ = self.backgroundStates.remove(.gettingNextPage) + } + } + }.asAnyCancellable() + + return state + } + } + + // MARK: - Get Next Page + + private func getNextPage(_ type: ImageType) async throws { + guard let itemID = item.id, hasNextPage else { return } + + let startIndex = currentPage * pageSize + let parameters = Paths.GetRemoteImagesParameters( + type: type, + startIndex: startIndex, + limit: pageSize, + isIncludeAllLanguages: includeAllLanguages + ) + + let request = Paths.getRemoteImages(itemID: itemID, parameters: parameters) + let response = try await userSession.client.send(request) + let newImages = response.value.images ?? [] + + hasNextPage = newImages.count >= pageSize + + await MainActor.run { + self.images.append(contentsOf: newImages) + currentPage += 1 + } + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index f22a0bceb..c411b4e16 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -69,8 +69,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 */; }; - 4E37F6132D17BB730022AADD /* LocalImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */; }; - 4E37F6142D17BB730022AADD /* LocalImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */; }; + 4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */; }; + 4E37F6172D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */; }; 4E3A24DA2CFE34A00083A72C /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */; }; 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; @@ -1226,7 +1226,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 = ""; }; - 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImageInfo.swift; sourceTree = ""; }; + 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfoViewModel.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 = ""; }; 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesViewModel.swift; sourceTree = ""; }; @@ -2252,6 +2252,25 @@ path = ServerLogsView; sourceTree = ""; }; + 4E37F6182D17EB220022AADD /* ItemImages */ = { + isa = PBXGroup; + children = ( + 4E4593A42D04E4D600E277E1 /* AddItemImageView */, + 4E4593A12D04E2A200E277E1 /* EditItemImagesView */, + ); + path = ItemImages; + sourceTree = ""; + }; + 4E37F6192D17EB3C0022AADD /* ItemMetadata */ = { + isa = PBXGroup; + children = ( + 4E5071E22CFCEFC3003FA2AD /* AddItemElementView */, + 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, + 4E6619FF2CEFE39000025C99 /* EditMetadataView */, + ); + path = ItemMetadata; + sourceTree = ""; + }; 4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */ = { isa = PBXGroup; children = ( @@ -2503,13 +2522,10 @@ 4E8F74A32CE03D3100CC8969 /* ItemEditorView */ = { isa = PBXGroup; children = ( - 4E5071E22CFCEFC3003FA2AD /* AddItemElementView */, 4E8F74A62CE03D4C00CC8969 /* Components */, - 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, - 4E4593A12D04E2A200E277E1 /* EditItemImagesView */, - 4E6619FF2CEFE39000025C99 /* EditMetadataView */, + 4E37F6182D17EB220022AADD /* ItemImages */, + 4E37F6192D17EB3C0022AADD /* ItemMetadata */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, - 4E4593A42D04E4D600E277E1 /* AddItemImageView */, ); path = ItemEditorView; sourceTree = ""; @@ -2527,8 +2543,9 @@ children = ( 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, - 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */, + 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, + 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */, ); path = ItemAdministration; sourceTree = ""; @@ -4438,7 +4455,6 @@ 531AC8BE26750DE20091C7EB /* ImageView.swift */, 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */, 4E24ECFA2D076F2B00A473A9 /* ListRowCheckbox.swift */, - 4E37F6122D17BB000022AADD /* LocalImageInfo.swift */, E1D37F472B9C648E00343D2B /* MaxHeightText.swift */, E1DC983F296DEBA500982F06 /* PosterIndicators */, E1FE69A628C29B720021BC93 /* ProgressBar.swift */, @@ -5460,6 +5476,7 @@ E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */, E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */, E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */, + 4E37F6172D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */, 4E35CE672CBED8B600DBD886 /* ServerTicks.swift in Sources */, E193D549271941CC00900D82 /* UserSignInView.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, @@ -5469,7 +5486,6 @@ 4E4E9C6B2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */, E1575E9B293E7B1E001665B1 /* EnvironmentValue+Keys.swift in Sources */, E133328929538D8D00EE76AB /* Files.swift in Sources */, - 4E37F6132D17BB730022AADD /* LocalImageInfo.swift in Sources */, E154967A296CB4B000C4EF88 /* VideoPlayerSettingsView.swift in Sources */, C46008742A97DFF2002B1C7A /* LiveLoadingOverlay.swift in Sources */, 4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */, @@ -5728,6 +5744,7 @@ C46DD8E02A8DC7790046A504 /* LiveOverlay.swift in Sources */, E111D8F828D03BF900400001 /* PagingLibraryView.swift in Sources */, E187F7672B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, + 4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */, 4E10C8172CC0455A0012CC9F /* CompatibilitiesSection.swift in Sources */, E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */, @@ -6156,7 +6173,6 @@ 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, E164A7F42BE4736300A54B18 /* SignOutIntervalSection.swift in Sources */, 4E8F74AF2CE03E2E00CC8969 /* RefreshMetadataButton.swift in Sources */, - 4E37F6142D17BB730022AADD /* LocalImageInfo.swift in Sources */, E148128528C15472003B8787 /* SortOrder+ItemSortOrder.swift in Sources */, E10231602BCF8B7E009D71FC /* VideoPlayerWrapperCoordinator.swift in Sources */, 4E4E9C6A2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift similarity index 86% rename from Swiftfin/Views/ItemEditorView/AddItemImageView/AddItemImageView.swift rename to Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index c96566a93..7e4086b47 100644 --- a/Swiftfin/Views/ItemEditorView/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -20,13 +20,16 @@ struct AddItemImageView: View { @Default(.accentColor) private var accentColor - // MARK: - State & Environment Objects + // MARK: - State, Observed, & Environment Objects @EnvironmentObject private var router: ItemEditorCoordinator.Router @StateObject - var viewModel: ItemImagesViewModel + private var viewModel: RemoteImageInfoViewModel + + @ObservedObject + private var updateViewModel: ItemImagesViewModel // MARK: - Dialog States @@ -45,15 +48,16 @@ struct AddItemImageView: View { // MARK: - Body - init(viewModel: ItemImagesViewModel) { + init(viewModel: RemoteImageInfoViewModel) { self._viewModel = StateObject(wrappedValue: viewModel) + self.updateViewModel = ItemImagesViewModel(item: viewModel.item) } // MARK: - Body var body: some View { contentView - .navigationBarTitle(viewModel.imageType?.rawValue.localizedCapitalized ?? L10n.unknown) + .navigationBarTitle(viewModel.imageType.rawValue.localizedCapitalized) .navigationBarTitleDisplayMode(.inline) .topBarTrailing { if viewModel.backgroundStates.contains(.refreshing) { @@ -61,15 +65,15 @@ struct AddItemImageView: View { } } .onFirstAppear { - if viewModel.state == .initial { - viewModel.send(.getImages) - } + viewModel.send(.refresh) } - .onReceive(viewModel.events) { event in + .onReceive(updateViewModel.events) { event in switch event { case .updated: UIDevice.feedback(.success) - router.dismissCoordinator() + router.pop() + case .deleted: + break case let .error(eventError): UIDevice.feedback(.error) error = eventError @@ -92,12 +96,10 @@ struct AddItemImageView: View { DelayedProgressView() case .content: gridView - case .updating: - updateView case let .error(error): ErrorView(error: error) .onRetry { - viewModel.send(.getImages) + viewModel.send(.refresh) } } } @@ -106,14 +108,14 @@ struct AddItemImageView: View { @ViewBuilder private var gridView: some View { - if viewModel.remoteImages.isEmpty { + if viewModel.images.isEmpty { Text(L10n.none) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .listRowSeparator(.hidden) .listRowInsets(.zero) } else { CollectionVGrid( - uniqueElements: viewModel.remoteImages, + uniqueElements: viewModel.images, layout: layout ) { image in imageButton(image) @@ -147,7 +149,7 @@ struct AddItemImageView: View { } label: { posterImage( image, - posterStyle: .landscape // image?.height ?? 0 > image?.width ?? 0 ? .portrait : .landscape + posterStyle: image?.height ?? 0 > image?.width ?? 0 ? .portrait : .landscape ) } } @@ -214,7 +216,7 @@ struct AddItemImageView: View { .topBarTrailing { Button(L10n.save) { if let newURL = image.url { - viewModel.send(.setImage(url: newURL)) + updateViewModel.send(.setImage(url: newURL, type: viewModel.imageType)) } selectedImage = nil } diff --git a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift similarity index 65% rename from Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift rename to Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index 8acf46781..be76742f6 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -15,6 +15,15 @@ import SwiftUI struct EditItemImagesView: View { + // MARK: - Selected Image Object + + private struct SelectedImage: Identifiable { + let id = UUID() + var image: UIImage + var type: ImageType + var index: Int + } + // MARK: - Defaults @Default(.accentColor) @@ -25,7 +34,7 @@ struct EditItemImagesView: View { @EnvironmentObject private var router: ItemEditorCoordinator.Router - @ObservedObject + @StateObject var viewModel: ItemImagesViewModel // MARK: - Dialog State @@ -33,10 +42,7 @@ struct EditItemImagesView: View { @State private var isImportingImage = false @State - private var isDeletingImage = false - - @State - private var selectedImage: LocalImageInfo? + private var selectedImage: SelectedImage? // MARK: - Error State @@ -59,21 +65,44 @@ struct EditItemImagesView: View { contentView .navigationBarTitle(L10n.replaceImages) .navigationBarTitleDisplayMode(.inline) - .onAppear { viewModel.send(.refresh) } - .sheet(item: $selectedImage) { item in - deletionSheet( - item.image!, - type: item.type!, - index: item.index ?? 0 - ) + .onFirstAppear { + viewModel.send(.refresh) + } + .sheet(item: $selectedImage) { input in + deletionSheet(input.image, type: input.type, index: input.index) } .fileImporter( isPresented: $isImportingImage, allowedContentTypes: [.image], allowsMultipleSelection: false ) { - handleFileImport(result: $0) + switch $0 { + case let .success(urls): + if let url = urls.first { + do { + let data = try Data(contentsOf: url) + if let image = UIImage(data: data) { + viewModel.send(.uploadImage(image: image, type: .primary)) + } + } catch { + self.error = JellyfinAPIError("Failed to load image data") + } + } + case let .failure(fileError): + self.error = fileError + } } + .onReceive(viewModel.events) { event in + switch event { + case .updated: + viewModel.send(.refresh) + case .deleted: + break + case let .error(eventError): + self.error = eventError + } + } + .errorMessage($error) } // MARK: - Content View @@ -95,15 +124,16 @@ struct EditItemImagesView: View { @ViewBuilder private func imageScrollView(for imageType: ImageType) -> some View { - if let images = viewModel.localImages[imageType.rawValue] { + if let images = viewModel.images[imageType.rawValue] { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { - ForEach(images, id: \.self) { image in + ForEach(images.indices, id: \.self) { index in + let image = images[index] imageButton(image) { - selectedImage = LocalImageInfo( - index: 0, + selectedImage = .init( image: image, - type: imageType + type: imageType, + index: index ) } } @@ -118,8 +148,14 @@ struct EditItemImagesView: View { HStack(alignment: .center, spacing: 16) { Text(imageType.rawValue.localizedCapitalized) Spacer() - Button(action: { viewModel.send(.setImageType(imageType)) - router.route(to: \.addImage, viewModel) + Button(action: { + router.route( + to: \.addImage, + RemoteImageInfoViewModel( + item: viewModel.item, + imageType: imageType + ) + ) }) { Image(systemName: "magnifyingglass") } @@ -128,9 +164,11 @@ struct EditItemImagesView: View { } } .font(.headline) - .padding(.horizontal, 16) + .padding(.horizontal, 30) } + // MARK: - Image Button + private func imageButton(_ image: UIImage, onSelect: @escaping () -> Void) -> some View { Button(action: onSelect) { ZStack { @@ -146,25 +184,6 @@ struct EditItemImagesView: View { } } - private func handleFileImport(result: Result<[URL], Error>) { - switch result { - case let .success(urls): - if let url = urls.first { - do { - let data = try Data(contentsOf: url) - if let image = UIImage(data: data) { - viewModel.send(.uploadImage(image: image)) - } - } catch { - print("Error loading image data: \(error.localizedDescription)") - } - } - case let .failure(fileError): - error = fileError - print("File import error: \(fileError.localizedDescription)") - } - } - // MARK: - Delete Image Confirmation @ViewBuilder @@ -176,11 +195,7 @@ struct EditItemImagesView: View { .scaledToFit() Text("\(Int(image.size.width)) x \(Int(image.size.height))") - .font(.body) - - Text(image.accessibilityIdentifier ?? "-") - .font(.caption) - .foregroundStyle(.secondary) + .font(.headline) } .padding(.horizontal) .navigationTitle(L10n.replaceImages) @@ -190,9 +205,7 @@ struct EditItemImagesView: View { } .topBarTrailing { Button(L10n.delete, role: .destructive) { - viewModel.send(.setImageType(type)) - viewModel.send(.deleteImage(index: index)) - viewModel.send(.setImageType(nil)) + viewModel.send(.deleteImage(type: type, index: index)) selectedImage = nil } .buttonStyle(.toolbarPill(.red)) diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/NameInput.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/Components/NameInput.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/SearchResultsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/Components/SearchResultsSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift similarity index 100% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift From 938cc2baef3d2272b50e62305b0318a53a37b4b8 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 22 Dec 2024 14:58:31 -0700 Subject: [PATCH 10/45] Refreshing is working but I might need to make this work mroe effiently... --- Shared/Strings/Strings.swift | 4 ++ .../ItemImagesViewModel.swift | 44 +++++++++++++++---- .../Views/ItemEditorView/ItemEditorView.swift | 2 +- .../EditItemImagesView.swift | 8 ++-- Translations/en.lproj/Localizable.strings | 6 +++ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index f64386563..4e7473f36 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -384,6 +384,8 @@ internal enum L10n { } /// Are you sure you wish to delete this device? This session will be logged out. internal static let deleteDeviceWarning = L10n.tr("Localizable", "deleteDeviceWarning", fallback: "Are you sure you wish to delete this device? This session will be logged out.") + /// Delete image + internal static let deleteImage = L10n.tr("Localizable", "deleteImage", fallback: "Delete image") /// Are you sure you want to delete this item? internal static let deleteItemConfirmation = L10n.tr("Localizable", "deleteItemConfirmation", fallback: "Are you sure you want to delete this item?") /// Are you sure you want to delete this item? This action cannot be undone. @@ -606,6 +608,8 @@ internal enum L10n { internal static let idle = L10n.tr("Localizable", "idle", fallback: "Idle") /// Illustrator internal static let illustrator = L10n.tr("Localizable", "illustrator", fallback: "Illustrator") + /// Images + internal static let images = L10n.tr("Localizable", "images", fallback: "Images") /// Indicators internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators") /// Inker diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 6d141f5ac..ab103f332 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -76,6 +76,19 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { self.item = item self.includeAllLanguages = includeAllLanguages super.init() + + Notifications[.itemMetadataDidChange] + .publisher + .sink { [weak self] item in + guard let self else { return } + self.item = item + Task { + await MainActor.run { + self.send(.refresh) + } + } + } + .store(in: &cancellables) } // MARK: - Respond to Actions @@ -91,7 +104,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { do { await MainActor.run { self.state = .initial - self.images.removeAll() _ = self.backgroundStates.append(.refreshing) } @@ -198,11 +210,19 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { // MARK: - Get All Images by Type - private func getAllImages() async throws { + private func getAllImages(for imageType: ImageType? = nil) async throws { guard let itemID = item.id else { return } + let imageTypesToProcess = imageType.map { [$0] } ?? ImageType.allCases + + await MainActor.run { + for type in imageTypesToProcess { + self.images[type.rawValue] = [] + } + } + let results = try await withThrowingTaskGroup(of: (String, [UIImage]).self) { group -> [String: [UIImage]] in - for imageType in ImageType.allCases { + for type in imageTypesToProcess { group.addTask { var images: [UIImage] = [] var index = 0 @@ -210,7 +230,11 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { while true { do { let parameters = Paths.GetItemImageParameters(imageIndex: index) - let request = Paths.getItemImage(itemID: itemID, imageType: imageType.rawValue, parameters: parameters) + let request = Paths.getItemImage( + itemID: itemID, + imageType: type.rawValue, + parameters: parameters + ) let response = try await self.userSession.client.send(request) if let image = UIImage(data: response.value) { @@ -223,7 +247,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } } - return (imageType.rawValue, images) + return (type.rawValue, images) } } @@ -235,7 +259,9 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } await MainActor.run { - self.images = results + for (key, images) in results { + self.images[key] = images + } } } @@ -297,12 +323,13 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { _ = try await userSession.client.send(request) + try await refreshItem() + await MainActor.run { if var typeImages = images[type.rawValue], index < typeImages.count { typeImages.remove(at: index) images[type.rawValue] = typeImages } - Notifications[.itemMetadataDidChange].post(item) } } @@ -322,9 +349,8 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { let response = try await userSession.client.send(request) await MainActor.run { - self.item = response.value _ = backgroundStates.remove(.refreshing) - Notifications[.itemMetadataDidChange].post(item) + Notifications[.itemMetadataDidChange].post(response.value) } } } diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index 24d25453e..fd9199667 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -98,7 +98,7 @@ struct ItemEditorView: View { @ViewBuilder private var editView: some View { Section(L10n.edit) { - ChevronButton(L10n.replaceImages) + ChevronButton(L10n.images) .onSelect { router.route(to: \.editImages, viewModel.item) } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index be76742f6..26029de40 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -63,7 +63,7 @@ struct EditItemImagesView: View { var body: some View { contentView - .navigationBarTitle(L10n.replaceImages) + .navigationBarTitle(L10n.images) .navigationBarTitleDisplayMode(.inline) .onFirstAppear { viewModel.send(.refresh) @@ -175,9 +175,9 @@ struct EditItemImagesView: View { Color.secondarySystemFill Image(uiImage: image) .resizable() - .scaledToFit() + .scaledToFill() } - .posterStyle(.landscape) + .posterStyle(image.size.height > image.size.width ? .portrait : .landscape) .frame(maxHeight: 150) .shadow(radius: 4) .padding(16) @@ -198,7 +198,7 @@ struct EditItemImagesView: View { .font(.headline) } .padding(.horizontal) - .navigationTitle(L10n.replaceImages) + .navigationTitle(L10n.deleteImage) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { selectedImage = nil diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 8eccb96c2..d6ab04c0a 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -541,6 +541,9 @@ /// Are you sure you wish to delete this device? This session will be logged out. "deleteDeviceWarning" = "Are you sure you wish to delete this device? This session will be logged out."; +/// Delete image +"deleteImage" = "Delete image"; + /// Are you sure you want to delete this item? "deleteItemConfirmation" = "Are you sure you want to delete this item?"; @@ -853,6 +856,9 @@ /// Illustrator "illustrator" = "Illustrator"; +/// Images +"images" = "Images"; + /// Indicators "indicators" = "Indicators"; From 26677c704d8acc761fa44b30617602c7fb93219e Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 22 Dec 2024 18:36:07 -0700 Subject: [PATCH 11/45] 90% There! --- Shared/Extensions/JellyfinAPI/ImageInfo.swift | 17 ++ .../ItemImagesViewModel.swift | 175 ++++++++++-------- .../RemoteImageInfoViewModel.swift | 76 ++++++++ Swiftfin.xcodeproj/project.pbxproj | 6 + .../AddItemImageView/AddItemImageView.swift | 24 +-- .../EditItemImagesView.swift | 86 ++++----- 6 files changed, 242 insertions(+), 142 deletions(-) create mode 100644 Shared/Extensions/JellyfinAPI/ImageInfo.swift diff --git a/Shared/Extensions/JellyfinAPI/ImageInfo.swift b/Shared/Extensions/JellyfinAPI/ImageInfo.swift new file mode 100644 index 000000000..50f832ff8 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/ImageInfo.swift @@ -0,0 +1,17 @@ +// +// 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 + +// TODO: How SHOULD I get Identifiable +extension ImageInfo: @retroactive Identifiable { + public var id: String { + self.imageTag?.hashString ?? UUID().uuidString + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index ab103f332..01f704084 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -11,7 +11,7 @@ import Foundation import IdentifiedCollections import JellyfinAPI import OrderedCollections -import UIKit +import SwiftUI class ItemImagesViewModel: ViewModel, Stateful, Eventful { @@ -23,9 +23,9 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { enum Action: Equatable { case refresh - case setImage(url: String, type: ImageType) + case backgroundRefresh case uploadImage(image: UIImage, type: ImageType, index: Int = 0) - case deleteImage(type: ImageType, index: Int = 0) + case deleteImage(ImageInfo) } enum BackgroundState: Hashable { @@ -49,7 +49,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { @Published var item: BaseItemDto @Published - var images: [String: [UIImage]] = [:] + var images: [ImageInfo: UIImage] = [:] // MARK: - State Management @@ -84,7 +84,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { self.item = item Task { await MainActor.run { - self.send(.refresh) + self.send(.backgroundRefresh) } } } @@ -96,27 +96,24 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { func respond(to action: Action) -> State { switch action { - case .refresh: + case .backgroundRefresh: task?.cancel() task = Task { [weak self] in guard let self else { return } do { await MainActor.run { - self.state = .initial _ = self.backgroundStates.append(.refreshing) } try await self.getAllImages() await MainActor.run { - self.state = .content _ = self.backgroundStates.remove(.refreshing) } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .error(apiError) self.eventSubject.send(.error(apiError)) _ = self.backgroundStates.remove(.refreshing) } @@ -125,27 +122,30 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .setImage(url, imageType): + case .refresh: task?.cancel() task = Task { [weak self] in - guard let self = self else { return } + guard let self else { return } do { await MainActor.run { - _ = self.state = .updating + self.state = .initial + _ = self.backgroundStates.append(.refreshing) + self.images.removeAll() } - try await self.setImage(url, type: imageType) + try await self.getAllImages() await MainActor.run { - self.eventSubject.send(.updated) - _ = self.state = .updating + self.state = .content + _ = self.backgroundStates.remove(.refreshing) } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { + self.state = .error(apiError) self.eventSubject.send(.error(apiError)) - _ = self.state = .updating + _ = self.backgroundStates.remove(.refreshing) } } }.asAnyCancellable() @@ -179,7 +179,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .deleteImage(imageType, index): + case let .deleteImage(imageInfo): task?.cancel() task = Task { [weak self] in @@ -189,7 +189,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { _ = self.state = .deleting } - try await self.deleteImage(imageType, index: index) + try await self.deleteImage(imageInfo) await MainActor.run { self.eventSubject.send(.deleted) @@ -208,73 +208,67 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } } - // MARK: - Get All Images by Type + // MARK: - Get All Item Images - private func getAllImages(for imageType: ImageType? = nil) async throws { + private func getAllImages() async throws { guard let itemID = item.id else { return } - let imageTypesToProcess = imageType.map { [$0] } ?? ImageType.allCases + // Get all of the ImageInfos for the Item + let imageRequest = Paths.getItemImageInfos(itemID: itemID) + let imageResponse = try await self.userSession.client.send(imageRequest) + let imageInfos = imageResponse.value + // Get Current vs New ImageInfos for comparison + let currentImageInfos = Set(self.images.keys) + let newImageInfos = Set(imageInfos) + + // Exit if all ImageInfos are the same + guard currentImageInfos != newImageInfos else { return } + + // Remove missing ImageInfos from published Images await MainActor.run { - for type in imageTypesToProcess { - self.images[type.rawValue] = [] - } + self.images = self.images.filter { newImageInfos.contains($0.key) } } - let results = try await withThrowingTaskGroup(of: (String, [UIImage]).self) { group -> [String: [UIImage]] in - for type in imageTypesToProcess { + // Identify missing ImageInfos in the published Images + let missingImageInfos = imageInfos.filter { !self.images.keys.contains($0) } + + // Get all UIImages for all missing ImageInfos + try await withThrowingTaskGroup(of: (ImageInfo, UIImage).self) { group in + for imageInfo in missingImageInfos { group.addTask { - var images: [UIImage] = [] - var index = 0 - - while true { - do { - let parameters = Paths.GetItemImageParameters(imageIndex: index) - let request = Paths.getItemImage( - itemID: itemID, - imageType: type.rawValue, - parameters: parameters - ) - let response = try await self.userSession.client.send(request) - - if let image = UIImage(data: response.value) { - images.append(image) - } - - index += 1 - } catch { - break + do { + let parameters = Paths.GetItemImageParameters( + tag: imageInfo.imageTag ?? "", + imageIndex: imageInfo.imageIndex + ) + let request = Paths.getItemImage( + itemID: itemID, + imageType: imageInfo.imageType?.rawValue ?? "", + parameters: parameters + ) + let response = try await self.userSession.client.send(request) + + // Convert the Response Data into a UIImage + if let image = UIImage(data: response.value) { + return (imageInfo, image) } + } catch { + throw JellyfinAPIError("Failed to fetch image for \(imageInfo): \(error)") } - - return (type.rawValue, images) + throw JellyfinAPIError("Failed to fetch image for \(imageInfo)") } } - var collectedResults: [String: [UIImage]] = [:] - for try await (key, images) in group { - collectedResults[key] = images - } - return collectedResults - } - - await MainActor.run { - for (key, images) in results { - self.images[key] = images + // Publish ImageInfos + for try await (imageInfo, image) in group { + await MainActor.run { + self.images[imageInfo] = image + } } } - } - // MARK: - Set Image From URL - - private func setImage(_ url: String, type: ImageType) async throws { - guard let itemID = item.id else { return } - - let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: url) - let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) - try await userSession.client.send(imageRequest) - - try await refreshItem() + try await orderImages() } // MARK: - Upload Image @@ -308,17 +302,20 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { _ = try await userSession.client.send(request) try await refreshItem() + try await orderImages() } // MARK: - Delete Image - private func deleteImage(_ type: ImageType, index: Int = 0) async throws { - guard let itemID = item.id else { return } + private func deleteImage(_ imageInfo: ImageInfo) async throws { + guard let itemID = item.id, + let imageType = imageInfo.imageType?.rawValue, + let imageIndex = imageInfo.imageIndex else { return } let request = Paths.deleteItemImageByIndex( itemID: itemID, - imageType: type.rawValue, - imageIndex: index + imageType: imageType, + imageIndex: imageIndex ) _ = try await userSession.client.send(request) @@ -326,9 +323,27 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { try await refreshItem() await MainActor.run { - if var typeImages = images[type.rawValue], index < typeImages.count { - typeImages.remove(at: index) - images[type.rawValue] = typeImages + self.images = images.filter { $0.key != imageInfo } + } + + try await orderImages() + } + + // MARK: - Order Images + + private func orderImages() async throws { + await MainActor.run { + self.images = self.images.sorted(by: { lhs, rhs in + guard let lhsType = lhs.key.imageType, let rhsType = rhs.key.imageType else { + return false + } + if lhsType != rhsType { + return lhsType.rawValue < rhsType.rawValue + } + return (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) + }) + .reduce(into: [ImageInfo: UIImage]()) { result, pair in + result[pair.key] = pair.value } } } @@ -336,7 +351,7 @@ class ItemImagesViewModel: 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 { _ = backgroundStates.append(.refreshing) @@ -344,13 +359,15 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { let request = Paths.getItem( userID: userSession.user.id, - itemID: itemId + itemID: itemID ) + let response = try await userSession.client.send(request) await MainActor.run { + self.item = response.value _ = backgroundStates.remove(.refreshing) - Notifications[.itemMetadataDidChange].post(response.value) + Notifications[.itemMetadataDidChange].post(item) } } } diff --git a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift index 6cbb547ab..4d02d244b 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift @@ -19,10 +19,16 @@ private let DefaultPageSize = 50 class RemoteImageInfoViewModel: ViewModel, Stateful { + enum Event: Equatable { + case updated + case error(JellyfinAPIError) + } + enum Action: Equatable { case cancel case refresh case getNextPage + case setImage(url: String, type: ImageType) } enum BackgroundState: Hashable { @@ -33,6 +39,7 @@ class RemoteImageInfoViewModel: ViewModel, Stateful { enum State: Hashable { case initial case content + case updating case error(JellyfinAPIError) } @@ -61,6 +68,13 @@ class RemoteImageInfoViewModel: ViewModel, Stateful { var backgroundStates: OrderedSet = [] private var task: AnyCancellable? + private let eventSubject = PassthroughSubject() + + // MARK: - Eventful + + var events: AnyPublisher { + eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher() + } // MARK: - Initializer @@ -146,6 +160,33 @@ class RemoteImageInfoViewModel: ViewModel, Stateful { } }.asAnyCancellable() + return state + + case let .setImage(url, imageType): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.state = .updating + } + + try await self.setImage(url, type: imageType) + + await MainActor.run { + self.eventSubject.send(.updated) + _ = self.state = .updating + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.eventSubject.send(.error(apiError)) + _ = self.state = .updating + } + } + }.asAnyCancellable() + return state } } @@ -174,4 +215,39 @@ class RemoteImageInfoViewModel: ViewModel, Stateful { currentPage += 1 } } + + // MARK: - Set Image From URL + + private func setImage(_ url: String, type: ImageType) async throws { + guard let itemID = item.id else { return } + + let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: url) + let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) + try await userSession.client.send(imageRequest) + + try await refreshItem() + } + + // MARK: - Refresh Item + + private func refreshItem() async throws { + guard let itemID = item.id else { return } + + await MainActor.run { + _ = 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 + _ = backgroundStates.remove(.refreshing) + Notifications[.itemMetadataDidChange].post(item) + } + } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index c411b4e16..01f0614f3 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 4E10C81D2CC046610012CC9F /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E10C81C2CC0465F0012CC9F /* UserSection.swift */; }; 4E11805F2CBF52380077A588 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; 4E12F9172CBE9619006C217E /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; }; + 4E13FAD82D18D5AF007785F6 /* ImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */; }; + 4E13FAD92D18D5AF007785F6 /* ImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */; }; 4E14DC032CD43DD2001B621B /* AdminDashboardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E14DC022CD43DCB001B621B /* AdminDashboardCoordinator.swift */; }; 4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */; }; 4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD522C01840C00110147 /* LetterPickerBar.swift */; }; @@ -1192,6 +1194,7 @@ 4E10C8182CC045690012CC9F /* CustomDeviceNameSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceNameSection.swift; sourceTree = ""; }; 4E10C81C2CC0465F0012CC9F /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; }; 4E12F9152CBE9615006C217E /* DeviceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceType.swift; sourceTree = ""; }; + 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageInfo.swift; sourceTree = ""; }; 4E14DC022CD43DCB001B621B /* AdminDashboardCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDashboardCoordinator.swift; sourceTree = ""; }; 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerButton.swift; sourceTree = ""; }; 4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = ""; }; @@ -4409,6 +4412,7 @@ E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */, 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */, E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */, + 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */, E1D842902933F87500D1041A /* ItemFields.swift */, E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, @@ -5227,6 +5231,7 @@ E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */, E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */, E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */, + 4E13FAD82D18D5AF007785F6 /* ImageInfo.swift in Sources */, E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */, C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */, E13D98EE2D0664C1005FE96D /* NotificationSet.swift in Sources */, @@ -5946,6 +5951,7 @@ C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */, 4E17498E2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */, 4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */, + 4E13FAD92D18D5AF007785F6 /* ImageInfo.swift in Sources */, 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */, E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */, E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 7e4086b47..4f18187ea 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -26,10 +26,7 @@ struct AddItemImageView: View { private var router: ItemEditorCoordinator.Router @StateObject - private var viewModel: RemoteImageInfoViewModel - - @ObservedObject - private var updateViewModel: ItemImagesViewModel + var viewModel: RemoteImageInfoViewModel // MARK: - Dialog States @@ -48,32 +45,24 @@ struct AddItemImageView: View { // MARK: - Body - init(viewModel: RemoteImageInfoViewModel) { - self._viewModel = StateObject(wrappedValue: viewModel) - self.updateViewModel = ItemImagesViewModel(item: viewModel.item) - } - - // MARK: - Body - var body: some View { contentView .navigationBarTitle(viewModel.imageType.rawValue.localizedCapitalized) .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(viewModel.state == .updating) .topBarTrailing { if viewModel.backgroundStates.contains(.refreshing) { - ProgramsView() + ProgressView() } } .onFirstAppear { viewModel.send(.refresh) } - .onReceive(updateViewModel.events) { event in + .onReceive(viewModel.events) { event in switch event { case .updated: UIDevice.feedback(.success) router.pop() - case .deleted: - break case let .error(eventError): UIDevice.feedback(.error) error = eventError @@ -96,6 +85,8 @@ struct AddItemImageView: View { DelayedProgressView() case .content: gridView + case .updating: + ProgressView() case let .error(error): ErrorView(error: error) .onRetry { @@ -182,7 +173,6 @@ struct AddItemImageView: View { .foregroundColor(.secondary) .font(.headline) } - .scaledToFit() .posterStyle(posterStyle) } @@ -216,7 +206,7 @@ struct AddItemImageView: View { .topBarTrailing { Button(L10n.save) { if let newURL = image.url { - updateViewModel.send(.setImage(url: newURL, type: viewModel.imageType)) + viewModel.send(.setImage(url: newURL, type: viewModel.imageType)) } selectedImage = nil } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index 26029de40..36d2ad98d 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -15,15 +15,6 @@ import SwiftUI struct EditItemImagesView: View { - // MARK: - Selected Image Object - - private struct SelectedImage: Identifiable { - let id = UUID() - var image: UIImage - var type: ImageType - var index: Int - } - // MARK: - Defaults @Default(.accentColor) @@ -42,14 +33,14 @@ struct EditItemImagesView: View { @State private var isImportingImage = false @State - private var selectedImage: SelectedImage? + private var selectedImage: ImageInfo? // MARK: - Error State @State private var error: Error? - // MARK: - Computed Properties + // MARK: - Ordered ImageTypes private var orderedItems: [ImageType] { ImageType.allCases.sorted { lhs, rhs in @@ -68,8 +59,8 @@ struct EditItemImagesView: View { .onFirstAppear { viewModel.send(.refresh) } - .sheet(item: $selectedImage) { input in - deletionSheet(input.image, type: input.type, index: input.index) + .sheet(item: $selectedImage) { imageInfo in + deletionSheet(imageInfo) } .fileImporter( isPresented: $isImportingImage, @@ -120,29 +111,28 @@ struct EditItemImagesView: View { } } - // MARK: - Helpers + // MARK: - Image Scrolle View @ViewBuilder private func imageScrollView(for imageType: ImageType) -> some View { - if let images = viewModel.images[imageType.rawValue] { + let filteredImages = viewModel.images.filter { $0.key.imageType == imageType } + let imageArray = Array(filteredImages) + + if !imageArray.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { - ForEach(images.indices, id: \.self) { index in - let image = images[index] - imageButton(image) { - selectedImage = .init( - image: image, - type: imageType, - index: index - ) + HStack { + ForEach(imageArray, id: \.key) { imageData in + imageButton(imageData.value) { + selectedImage = imageData.key } } } - .padding(.horizontal, 16) } } } + // MARK: - Section Header + @ViewBuilder private func sectionHeader(for imageType: ImageType) -> some View { HStack(alignment: .center, spacing: 16) { @@ -184,32 +174,36 @@ struct EditItemImagesView: View { } } - // MARK: - Delete Image Confirmation + // MARK: - Delete Image Confirmation Sheet @ViewBuilder - private func deletionSheet(_ image: UIImage, type: ImageType, index: Int) -> some View { - NavigationView { - VStack { - Image(uiImage: image) - .resizable() - .scaledToFit() - - Text("\(Int(image.size.width)) x \(Int(image.size.height))") - .font(.headline) - } - .padding(.horizontal) - .navigationTitle(L10n.deleteImage) - .navigationBarTitleDisplayMode(.inline) - .navigationBarCloseButton { - selectedImage = nil - } - .topBarTrailing { - Button(L10n.delete, role: .destructive) { - viewModel.send(.deleteImage(type: type, index: index)) + private func deletionSheet(_ imageInfo: ImageInfo) -> some View { + if let image = viewModel.images[imageInfo] { + NavigationView { + VStack { + Image(uiImage: image) + .resizable() + .scaledToFit() + + Text("\(Int(image.size.width)) x \(Int(image.size.height))") + .font(.headline) + } + .padding(.horizontal) + .navigationTitle(L10n.deleteImage) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { selectedImage = nil } - .buttonStyle(.toolbarPill(.red)) + .topBarTrailing { + Button(L10n.delete, role: .destructive) { + viewModel.send(.deleteImage(imageInfo)) + selectedImage = nil + } + .buttonStyle(.toolbarPill(.red)) + } } + } else { + ErrorView(error: JellyfinAPIError(L10n.unknownError)) } } } From bccfcef8bbe2b77c4c63aeab9893a305a51287c1 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 22 Dec 2024 18:52:57 -0700 Subject: [PATCH 12/45] Ability to cancel the update --- .../ItemImages/AddItemImageView/AddItemImageView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 4f18187ea..316b94f03 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -86,7 +86,7 @@ struct AddItemImageView: View { case .content: gridView case .updating: - ProgressView() + updateView case let .error(error): ErrorView(error: error) .onRetry { @@ -110,7 +110,6 @@ struct AddItemImageView: View { layout: layout ) { image in imageButton(image) - .padding(.vertical, 4) } .onReachedBottomEdge(offset: .offset(300)) { viewModel.send(.getNextPage) From 9949b720bd9d93082c8141a5a59ab88878f8b43f Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 22 Dec 2024 19:28:08 -0700 Subject: [PATCH 13/45] Still no luck uploading images? --- .../ItemImagesViewModel.swift | 38 ++++++++----------- .../EditItemImagesView.swift | 16 ++++---- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 01f704084..7df7e63cc 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -24,7 +24,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { enum Action: Equatable { case refresh case backgroundRefresh - case uploadImage(image: UIImage, type: ImageType, index: Int = 0) + case uploadImage(url: URL, type: ImageType) case deleteImage(ImageInfo) } @@ -152,7 +152,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .uploadImage(image, imageType, index): + case let .uploadImage(url, imageType): task?.cancel() task = Task { [weak self] in @@ -162,7 +162,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { _ = self.state = .updating } - try await self.uploadImage(image, type: imageType, index: index) + try await self.uploadImage(url, type: imageType) await MainActor.run { self.eventSubject.send(.updated) @@ -267,16 +267,22 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } } } - - try await orderImages() } // MARK: - Upload Image // TODO: Make actually work. 500 error. Bad format. - private func uploadImage(_ image: UIImage, type: ImageType, index: Int = 0) async throws { + private func uploadImage(_ url: URL, type: ImageType) async throws { guard let itemID = item.id else { return } + // Start accessing security-scoped resource + guard url.startAccessingSecurityScopedResource() else { + throw JellyfinAPIError("Unable to access file at \(url)") + } + defer { url.stopAccessingSecurityScopedResource() } + + let data = try Data(contentsOf: url) + let image = UIImage(data: data)! let contentType: String let imageData: Data @@ -287,22 +293,20 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { contentType = "image/jpeg" imageData = jpgData } else { - logger.error("Unable to upload the selected image") + logger.error("Unable to convert given profile image to png/jpg") throw JellyfinAPIError("An internal error occurred") } - var request = Paths.setItemImageByIndex( + var request = Paths.setItemImage( itemID: itemID, imageType: type.rawValue, - imageIndex: index, - imageData.base64EncodedData() + imageData ) request.headers = ["Content-Type": contentType] _ = try await userSession.client.send(request) try await refreshItem() - try await orderImages() } // MARK: - Delete Image @@ -323,17 +327,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { try await refreshItem() await MainActor.run { - self.images = images.filter { $0.key != imageInfo } - } - - try await orderImages() - } - - // MARK: - Order Images - - private func orderImages() async throws { - await MainActor.run { - self.images = self.images.sorted(by: { lhs, rhs in + self.images = images.filter { $0.key != imageInfo }.sorted(by: { lhs, rhs in guard let lhsType = lhs.key.imageType, let rhsType = rhs.key.imageType else { return false } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index 36d2ad98d..084f77a24 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -30,10 +30,10 @@ struct EditItemImagesView: View { // MARK: - Dialog State - @State - private var isImportingImage = false @State private var selectedImage: ImageInfo? + @State + private var uploadType: ImageType? // MARK: - Error State @@ -63,7 +63,7 @@ struct EditItemImagesView: View { deletionSheet(imageInfo) } .fileImporter( - isPresented: $isImportingImage, + isPresented: .constant(uploadType != nil), allowedContentTypes: [.image], allowsMultipleSelection: false ) { @@ -71,12 +71,10 @@ struct EditItemImagesView: View { case let .success(urls): if let url = urls.first { do { - let data = try Data(contentsOf: url) - if let image = UIImage(data: data) { - viewModel.send(.uploadImage(image: image, type: .primary)) + if let uploadType { + viewModel.send(.uploadImage(url: url, type: uploadType)) } - } catch { - self.error = JellyfinAPIError("Failed to load image data") + uploadType = nil } } case let .failure(fileError): @@ -149,7 +147,7 @@ struct EditItemImagesView: View { }) { Image(systemName: "magnifyingglass") } - Button(action: { isImportingImage = true }) { + Button(action: { uploadType = imageType }) { Image(systemName: "plus") } } From 1dac7cddeb0e3a8f07ad34e5c6294adda18edb81 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 28 Dec 2024 20:10:44 -0700 Subject: [PATCH 14/45] Stop reordering on deletion/addition --- .../ItemImagesViewModel.swift | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 7df7e63cc..b2210f5a7 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -327,18 +327,30 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { try await refreshItem() await MainActor.run { - self.images = images.filter { $0.key != imageInfo }.sorted(by: { lhs, rhs in - guard let lhsType = lhs.key.imageType, let rhsType = rhs.key.imageType else { - return false + self.images.removeValue(forKey: imageInfo) + + let updatedImages = self.images + .sorted { lhs, rhs in + guard let lhsType = lhs.key.imageType, let rhsType = rhs.key.imageType else { + return false + } + if lhsType != rhsType { + return lhsType.rawValue < rhsType.rawValue + } + return (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) } - if lhsType != rhsType { - return lhsType.rawValue < rhsType.rawValue + .reduce(into: [ImageInfo: UIImage]()) { result, pair in + var updatedInfo = pair.key + if updatedInfo.imageType == imageInfo.imageType, + let index = updatedInfo.imageIndex, + index > imageInfo.imageIndex! + { + updatedInfo.imageIndex = index - 1 + } + result[updatedInfo] = pair.value } - return (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) - }) - .reduce(into: [ImageInfo: UIImage]()) { result, pair in - result[pair.key] = pair.value - } + + self.images = updatedImages } } From 6ee6b12fbab5caaebfc484229f826b14509dfafe Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 3 Jan 2025 08:14:08 -0700 Subject: [PATCH 15/45] 2025 disclaimers --- PreferencesView/Sources/PreferencesView/Box.swift | 2 +- .../Sources/PreferencesView/KeyCommandAction.swift | 2 +- .../Sources/PreferencesView/KeyCommandsBuilder.swift | 2 +- .../Sources/PreferencesView/PreferenceKeys.swift | 2 +- .../Sources/PreferencesView/PreferencesView.swift | 2 +- .../Sources/PreferencesView/PressCommandAction.swift | 2 +- .../Sources/PreferencesView/PressCommandBuilder.swift | 2 +- .../PreferencesView/UIPreferencesHostingController.swift | 2 +- .../PreferencesView/UIViewController+Swizzling.swift | 2 +- .../Sources/PreferencesView/ViewExtensions.swift | 2 +- RedrawOnNotificationView.swift | 2 +- Scripts/Translations/AlphabetizeStrings.swift | 2 +- Scripts/Translations/PurgeUnusedStrings.swift | 2 +- Shared/AppIcons/AppIcons.swift | 2 +- Shared/AppIcons/DarkAppIcon.swift | 2 +- Shared/AppIcons/InvertedDarkAppIcon.swift | 2 +- Shared/AppIcons/InvertedLightAppIcon.swift | 2 +- Shared/AppIcons/LightAppIcon.swift | 2 +- Shared/AppIcons/PrimaryAppIcon.swift | 2 +- Shared/Components/AlternateLayoutView.swift | 2 +- Shared/Components/AssertionFailureView.swift | 2 +- Shared/Components/BlurView.swift | 2 +- Shared/Components/BulletedList.swift | 2 +- Shared/Components/ChevronAlertButton.swift | 2 +- Shared/Components/ChevronButton.swift | 2 +- Shared/Components/FastSVGView.swift | 2 +- Shared/Components/ImageView.swift | 2 +- Shared/Components/LetterPickerOrientation.swift | 2 +- Shared/Components/ListRowCheckbox.swift | 2 +- Shared/Components/MaxHeightText.swift | 2 +- Shared/Components/PosterIndicators/FavoriteIndicator.swift | 2 +- Shared/Components/PosterIndicators/ProgressIndicator.swift | 2 +- Shared/Components/PosterIndicators/UnwatchedIndicator.swift | 2 +- Shared/Components/PosterIndicators/WatchedIndicator.swift | 2 +- Shared/Components/ProgressBar.swift | 2 +- Shared/Components/RotateContentView.swift | 2 +- Shared/Components/RowDivider.swift | 2 +- Shared/Components/SelectorView.swift | 2 +- Shared/Components/SeparatorHStack.swift | 2 +- Shared/Components/SeparatorVStack.swift | 2 +- Shared/Components/SystemImageContentView.swift | 2 +- Shared/Components/TextPairView.swift | 2 +- Shared/Components/TruncatedText.swift | 2 +- .../Components/UserProfileImage/UserProfileHeroImage.swift | 2 +- Shared/Components/UserProfileImage/UserProfileImage.swift | 2 +- Shared/Components/WrappedView.swift | 2 +- Shared/Coordinators/AdminDashboardCoordinator.swift | 2 +- Shared/Coordinators/AppSettingsCoordinator.swift | 2 +- Shared/Coordinators/BasicNavigationCoordinator.swift | 2 +- Shared/Coordinators/CustomDeviceProfileCoordinator.swift | 2 +- Shared/Coordinators/CustomizeSettingsCoordinator.swift | 2 +- Shared/Coordinators/DownloadListCoordinator.swift | 2 +- Shared/Coordinators/DownloadTaskCoordinator.swift | 2 +- .../Coordinators/EditCustomDeviceProfileCoordinator.swift | 2 +- Shared/Coordinators/FilterCoordinator.swift | 2 +- Shared/Coordinators/HomeCoordinator.swift | 2 +- Shared/Coordinators/ItemCoordinator.swift | 2 +- Shared/Coordinators/ItemEditorCoordinator.swift | 2 +- Shared/Coordinators/LibraryCoordinator.swift | 2 +- .../LiveTVCoordinator/iOSLiveTVCoordinator.swift | 2 +- .../LiveTVCoordinator/tvOSLiveTVCoordinator.swift | 2 +- Shared/Coordinators/LiveVideoPlayerCoordinator.swift | 2 +- .../Coordinators/MainCoordinator/iOSMainCoordinator.swift | 2 +- .../MainCoordinator/iOSMainTabCoordinator.swift | 2 +- .../Coordinators/MainCoordinator/tvOSMainCoordinator.swift | 2 +- .../MainCoordinator/tvOSMainTabCoordinator.swift | 2 +- Shared/Coordinators/MediaCoordinator.swift | 2 +- Shared/Coordinators/MediaSourceInfoCoordinator.swift | 2 +- .../Coordinators/PlaybackQualitySettingsCoordinator.swift | 2 +- Shared/Coordinators/PlaybackSettingsCoordinator.swift | 2 +- Shared/Coordinators/SearchCoordinator.swift | 2 +- Shared/Coordinators/SelectUserCoordinator.swift | 2 +- Shared/Coordinators/SettingsCoordinator.swift | 2 +- Shared/Coordinators/UserProfileImageCoordinator.swift | 2 +- Shared/Coordinators/UserSignInCoordinator.swift | 2 +- Shared/Coordinators/VideoPlayerCoordinator.swift | 2 +- Shared/Coordinators/VideoPlayerSettingsCoordinator.swift | 2 +- Shared/Coordinators/VideoPlayerWrapperCoordinator.swift | 2 +- Shared/Errors/NetworkError.swift | 2 +- Shared/Extensions/Array.swift | 2 +- Shared/Extensions/Binding.swift | 2 +- Shared/Extensions/Button.swift | 2 +- Shared/Extensions/CGPoint.swift | 2 +- Shared/Extensions/CGSize.swift | 2 +- Shared/Extensions/Collection.swift | 2 +- Shared/Extensions/Color.swift | 2 +- Shared/Extensions/CoreStore.swift | 2 +- Shared/Extensions/Dictionary.swift | 2 +- Shared/Extensions/Double.swift | 2 +- Shared/Extensions/Edge.swift | 2 +- Shared/Extensions/EdgeInsets.swift | 2 +- .../Extensions/EnvironmentValue/EnvironmentValue+Keys.swift | 2 +- .../EnvironmentValue/EnvironmentValue+Values.swift | 2 +- Shared/Extensions/Equatable.swift | 2 +- Shared/Extensions/Files.swift | 2 +- Shared/Extensions/Font.swift | 2 +- Shared/Extensions/FormatStyle.swift | 2 +- Shared/Extensions/Hashable.swift | 2 +- Shared/Extensions/HorizontalAlignment.swift | 2 +- Shared/Extensions/Int.swift | 2 +- Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift | 2 +- .../JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift | 2 +- .../JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift | 2 +- .../BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift | 2 +- Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift | 2 +- .../JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift | 2 +- .../JellyfinAPI/BaseItemPerson/BaseItemPerson.swift | 2 +- Shared/Extensions/JellyfinAPI/ChapterInfo.swift | 2 +- Shared/Extensions/JellyfinAPI/CodecProfile.swift | 2 +- Shared/Extensions/JellyfinAPI/DayOfWeek.swift | 2 +- Shared/Extensions/JellyfinAPI/DeviceInfo.swift | 2 +- Shared/Extensions/JellyfinAPI/DeviceProfile.swift | 2 +- Shared/Extensions/JellyfinAPI/DeviceType.swift | 2 +- Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift | 2 +- Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift | 2 +- Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift | 2 +- Shared/Extensions/JellyfinAPI/ImageInfo.swift | 2 +- Shared/Extensions/JellyfinAPI/ItemFields.swift | 2 +- Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift | 2 +- Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift | 2 +- Shared/Extensions/JellyfinAPI/JellyfinClient.swift | 2 +- Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift | 2 +- Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift | 2 +- .../MediaSourceInfo+ItemVideoPlayerViewModel.swift | 2 +- .../JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift | 2 +- Shared/Extensions/JellyfinAPI/MediaStream.swift | 2 +- Shared/Extensions/JellyfinAPI/MetadataField.swift | 2 +- Shared/Extensions/JellyfinAPI/NameGuidPair.swift | 2 +- Shared/Extensions/JellyfinAPI/ParentalRating.swift | 2 +- Shared/Extensions/JellyfinAPI/PersonKind.swift | 2 +- Shared/Extensions/JellyfinAPI/PlayMethod.swift | 2 +- Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift | 2 +- Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift | 2 +- Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift | 2 +- Shared/Extensions/JellyfinAPI/ServerTicks.swift | 2 +- Shared/Extensions/JellyfinAPI/SessionInfo.swift | 2 +- Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift | 2 +- Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift | 2 +- Shared/Extensions/JellyfinAPI/SubtitleProfile.swift | 2 +- Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift | 2 +- Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift | 2 +- Shared/Extensions/JellyfinAPI/TaskState.swift | 2 +- Shared/Extensions/JellyfinAPI/TaskTriggerType.swift | 2 +- Shared/Extensions/JellyfinAPI/TranscodeReason.swift | 2 +- Shared/Extensions/JellyfinAPI/TranscodingProfile.swift | 2 +- Shared/Extensions/JellyfinAPI/UserDto.swift | 2 +- Shared/Extensions/JellyfinAPI/Video3DFormat.swift | 2 +- Shared/Extensions/NavigationCoordinatable.swift | 2 +- Shared/Extensions/Nuke/DataCache.swift | 2 +- Shared/Extensions/Nuke/ImagePipeline.swift | 2 +- Shared/Extensions/Optional.swift | 2 +- Shared/Extensions/OrderedDictionary.swift | 2 +- Shared/Extensions/PersistentLogHandler.swift | 2 +- Shared/Extensions/Sequence.swift | 2 +- Shared/Extensions/Set.swift | 2 +- Shared/Extensions/String.swift | 2 +- Shared/Extensions/Task.swift | 2 +- Shared/Extensions/Text.swift | 2 +- Shared/Extensions/UIApplication.swift | 2 +- Shared/Extensions/UIColor.swift | 2 +- Shared/Extensions/UIDevice.swift | 2 +- Shared/Extensions/UIGestureRecognizer.swift | 2 +- Shared/Extensions/UIHostingController.swift | 2 +- Shared/Extensions/UIScreen.swift | 2 +- Shared/Extensions/URL.swift | 2 +- Shared/Extensions/URLComponents.swift | 2 +- Shared/Extensions/URLResponse.swift | 2 +- Shared/Extensions/URLSessionConfiguration.swift | 2 +- Shared/Extensions/VerticalAlignment.swift | 2 +- .../Backport/BackPort+ScrollIndicatorVisibility.swift | 2 +- Shared/Extensions/ViewExtensions/Backport/Backport.swift | 2 +- .../ViewExtensions/Modifiers/AttributeStyleModifier.swift | 2 +- .../Modifiers/BackgroundParallaxHeaderModifier.swift | 2 +- .../Modifiers/BottomEdgeGradientModifier.swift | 2 +- .../Extensions/ViewExtensions/Modifiers/ErrorMessage.swift | 2 +- .../ViewExtensions/Modifiers/OnFinalDisappearModifier.swift | 2 +- .../ViewExtensions/Modifiers/OnFirstAppearModifier.swift | 2 +- .../Modifiers/OnReceiveNotificationModifier.swift | 2 +- .../Modifiers/OnScenePhaseChangedModifier.swift | 2 +- .../ViewExtensions/Modifiers/OnSizeChangedModifier.swift | 2 +- .../Modifiers/ScrollIfLargerThanContainerModifier.swift | 2 +- .../ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift | 2 +- .../Modifiers/SinceLastDisappearModifier.swift | 2 +- Shared/Extensions/ViewExtensions/PreferenceKeys.swift | 2 +- Shared/Extensions/ViewExtensions/ViewExtensions.swift | 2 +- Shared/Objects/AppAppearance.swift | 2 +- Shared/Objects/ArrayBuilder.swift | 2 +- Shared/Objects/BindingBox.swift | 2 +- Shared/Objects/CaseIterablePicker.swift | 2 +- Shared/Objects/ChannelProgram.swift | 2 +- Shared/Objects/CommaStringBuilder.swift | 2 +- Shared/Objects/CurrentDate.swift | 2 +- Shared/Objects/CustomDeviceProfileAction.swift | 2 +- Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift | 2 +- Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift | 2 +- Shared/Objects/Displayable.swift | 2 +- Shared/Objects/Eventful.swift | 2 +- Shared/Objects/GestureAction.swift | 2 +- Shared/Objects/ImageSource.swift | 2 +- Shared/Objects/ItemArrayElements.swift | 2 +- Shared/Objects/ItemFilter/AnyItemFilter.swift | 2 +- Shared/Objects/ItemFilter/ItemFilter.swift | 2 +- Shared/Objects/ItemFilter/ItemFilterCollection.swift | 2 +- Shared/Objects/ItemFilter/ItemFilterType.swift | 2 +- Shared/Objects/ItemFilter/ItemGenre.swift | 2 +- Shared/Objects/ItemFilter/ItemLetter.swift | 2 +- Shared/Objects/ItemFilter/ItemSortBy.swift | 2 +- Shared/Objects/ItemFilter/ItemTag.swift | 2 +- Shared/Objects/ItemFilter/ItemYear.swift | 2 +- Shared/Objects/ItemViewType.swift | 2 +- Shared/Objects/LibraryDisplayType.swift | 2 +- Shared/Objects/LibraryParent/LibraryParent.swift | 2 +- Shared/Objects/LibraryParent/TitledLibraryParent.swift | 2 +- Shared/Objects/MediaComponents/AudoCodec.swift | 2 +- Shared/Objects/MediaComponents/MediaContainer.swift | 2 +- Shared/Objects/MediaComponents/SubtitleFormat.swift | 2 +- Shared/Objects/MediaComponents/VideoCodec.swift | 2 +- Shared/Objects/NotificationSet.swift | 2 +- Shared/Objects/OverlayType.swift | 2 +- Shared/Objects/PanDirectionGestureRecognizer.swift | 2 +- Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift | 2 +- .../Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift | 2 +- .../PlaybackCompatibility/PlaybackCompatibility+Video.swift | 2 +- .../PlaybackCompatibility/PlaybackCompatibility.swift | 2 +- Shared/Objects/PlaybackDeviceProfile.swift | 2 +- Shared/Objects/PlaybackSpeed.swift | 2 +- Shared/Objects/Poster.swift | 2 +- Shared/Objects/PosterDisplayType.swift | 2 +- Shared/Objects/RepeatingTimer.swift | 2 +- Shared/Objects/RoundedCorner.swift | 2 +- Shared/Objects/ScalingButtonStyle.swift | 2 +- Shared/Objects/SelectUserServerSelection.swift | 2 +- Shared/Objects/SeriesStatus.swift | 2 +- Shared/Objects/SliderType.swift | 2 +- Shared/Objects/Stateful.swift | 2 +- Shared/Objects/Storable.swift | 2 +- Shared/Objects/StreamType.swift | 2 +- Shared/Objects/SupportedCaseIterable.swift | 2 +- Shared/Objects/SystemImageable.swift | 2 +- Shared/Objects/TextPair.swift | 2 +- Shared/Objects/TimeStampType.swift | 2 +- Shared/Objects/TimerProxy.swift | 2 +- Shared/Objects/TrailingTimestampType.swift | 2 +- Shared/Objects/Trie.swift | 2 +- Shared/Objects/UserAccessPolicy.swift | 2 +- Shared/Objects/UserPermissions.swift | 2 +- Shared/Objects/UserSignInState.swift | 2 +- Shared/Objects/Utilities.swift | 2 +- Shared/Objects/VideoPlayerActionButton.swift | 2 +- Shared/Objects/VideoPlayerJumpLength.swift | 2 +- Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift | 2 +- Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift | 2 +- .../Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift | 2 +- Shared/Objects/VideoPlayerType/VideoPlayerType.swift | 2 +- Shared/ServerDiscovery/ServerDiscovery.swift | 2 +- Shared/ServerDiscovery/ServerResponse.swift | 2 +- Shared/Services/DownloadManager.swift | 2 +- Shared/Services/DownloadTask.swift | 2 +- Shared/Services/Keychain.swift | 2 +- Shared/Services/LogManager.swift | 2 +- Shared/Services/Notifications.swift | 2 +- Shared/Services/SwiftfinDefaults.swift | 2 +- Shared/Services/UserSession.swift | 2 +- Shared/Strings/Strings.swift | 6 ------ Shared/SwiftfinStore/StoredValue/StoredValue.swift | 2 +- Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift | 2 +- Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift | 2 +- Shared/SwiftfinStore/StoredValue/StoredValues+User.swift | 2 +- Shared/SwiftfinStore/SwiftfinStore+Mappings.swift | 2 +- Shared/SwiftfinStore/SwiftfinStore+ServerState.swift | 2 +- Shared/SwiftfinStore/SwiftfinStore.swift | 2 +- Shared/SwiftfinStore/SwiftinStore+UserState.swift | 2 +- Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift | 2 +- Shared/SwiftfinStore/V1Schema/V1ServerModel.swift | 2 +- Shared/SwiftfinStore/V1Schema/V1UserModel.swift | 2 +- Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift | 2 +- Shared/SwiftfinStore/V2Schema/V2AnyData.swift | 2 +- Shared/SwiftfinStore/V2Schema/V2ServerModel.swift | 2 +- Shared/SwiftfinStore/V2Schema/V2UserModel.swift | 2 +- Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift | 2 +- .../ViewModels/AdminDashboard/ActiveSessionsViewModel.swift | 2 +- .../ViewModels/AdminDashboard/AddServerUserViewModel.swift | 2 +- .../ViewModels/AdminDashboard/DeviceDetailViewModel.swift | 2 +- Shared/ViewModels/AdminDashboard/DevicesViewModel.swift | 2 +- Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift | 2 +- Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift | 2 +- .../AdminDashboard/ServerUserAdminViewModel.swift | 2 +- Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift | 2 +- Shared/ViewModels/ChannelLibraryViewModel.swift | 2 +- Shared/ViewModels/ConnectToServerViewModel.swift | 2 +- Shared/ViewModels/DownloadListViewModel.swift | 2 +- Shared/ViewModels/FilterViewModel.swift | 2 +- Shared/ViewModels/HomeViewModel.swift | 2 +- .../ViewModels/ItemAdministration/DeleteItemViewModel.swift | 2 +- .../ItemAdministration/IdentifyItemViewModel.swift | 2 +- .../ItemEditorViewModel/GenreEditorViewModel.swift | 2 +- .../ItemEditorViewModel/ItemEditorViewModel.swift | 2 +- .../ItemEditorViewModel/PeopleEditorViewModel.swift | 2 +- .../ItemEditorViewModel/StudioEditorViewModel.swift | 2 +- .../ItemEditorViewModel/TagEditorViewModel.swift | 2 +- .../ViewModels/ItemAdministration/ItemImagesViewModel.swift | 2 +- .../ItemAdministration/RefreshMetadataViewModel.swift | 2 +- .../ItemAdministration/RemoteImageInfoViewModel.swift | 2 +- .../ViewModels/ItemViewModel/CollectionItemViewModel.swift | 2 +- Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift | 2 +- Shared/ViewModels/ItemViewModel/ItemViewModel.swift | 2 +- Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift | 2 +- Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift | 2 +- Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift | 2 +- .../ViewModels/LibraryViewModel/ItemLibraryViewModel.swift | 2 +- .../LibraryViewModel/ItemTypeLibraryViewModel.swift | 2 +- .../LibraryViewModel/LatestInLibraryViewModel.swift | 2 +- .../LibraryViewModel/NextUpLibraryViewModel.swift | 2 +- .../LibraryViewModel/PagingLibraryViewModel.swift | 2 +- .../LibraryViewModel/RecentlyAddedViewModel.swift | 2 +- Shared/ViewModels/LiveVideoPlayerManager.swift | 2 +- Shared/ViewModels/MediaViewModel/MediaType.swift | 2 +- Shared/ViewModels/MediaViewModel/MediaViewModel.swift | 2 +- Shared/ViewModels/ParentalRatingsViewModel.swift | 2 +- Shared/ViewModels/ProgramsViewModel.swift | 2 +- Shared/ViewModels/QuickConnectAuthorizeViewModel.swift | 2 +- Shared/ViewModels/ResetUserPasswordViewModel.swift | 2 +- Shared/ViewModels/SearchViewModel.swift | 2 +- Shared/ViewModels/SelectUserViewModel.swift | 2 +- Shared/ViewModels/ServerCheckViewModel.swift | 2 +- Shared/ViewModels/ServerConnectionViewModel.swift | 2 +- Shared/ViewModels/ServerLogsViewModel.swift | 2 +- Shared/ViewModels/SettingsViewModel.swift | 2 +- Shared/ViewModels/UserLocalSecurityViewModel.swift | 2 +- Shared/ViewModels/UserProfileImageViewModel.swift | 2 +- Shared/ViewModels/UserSignInViewModel.swift | 2 +- .../VideoPlayerManager/DownloadVideoPlayerManager.swift | 2 +- .../VideoPlayerManager/OnlineVideoPlayerManager.swift | 2 +- .../ViewModels/VideoPlayerManager/VideoPlayerManager.swift | 2 +- Shared/ViewModels/VideoPlayerViewModel.swift | 2 +- Shared/ViewModels/ViewModel.swift | 2 +- .../PreferenceUIHosting/PreferenceUIHostingController.swift | 2 +- .../PreferenceUIHosting/PreferenceUIHostingSwizzling.swift | 2 +- Swiftfin tvOS/App/SwiftfinApp.swift | 2 +- Swiftfin tvOS/Components/CinematicBackgroundView.swift | 2 +- Swiftfin tvOS/Components/CinematicItemSelector.swift | 2 +- Swiftfin tvOS/Components/DotHStack.swift | 2 +- Swiftfin tvOS/Components/EnumPickerView.swift | 2 +- Swiftfin tvOS/Components/InlineEnumToggle.swift | 2 +- Swiftfin tvOS/Components/LandscapePosterProgressBar.swift | 2 +- Swiftfin tvOS/Components/ListRowButton.swift | 2 +- Swiftfin tvOS/Components/NonePosterButton.swift | 2 +- Swiftfin tvOS/Components/OrderedSectionSelectorView.swift | 2 +- Swiftfin tvOS/Components/PosterButton.swift | 2 +- Swiftfin tvOS/Components/PosterHStack.swift | 2 +- Swiftfin tvOS/Components/SFSymbolButton.swift | 2 +- Swiftfin tvOS/Components/SeeAllPosterButton.swift | 2 +- Swiftfin tvOS/Components/ServerButton.swift | 2 +- Swiftfin tvOS/Components/SplitFormWindowView.swift | 2 +- Swiftfin tvOS/Components/SplitLoginWindowView.swift | 2 +- Swiftfin tvOS/Components/StepperView.swift | 2 +- .../Extensions/View/Modifiers/NavigationBarMenuButton.swift | 2 +- Swiftfin tvOS/Extensions/View/View-tvOS.swift | 2 +- Swiftfin tvOS/ImageButtonStyle.swift | 2 +- Swiftfin tvOS/Objects/FocusGuide.swift | 2 +- Swiftfin tvOS/Views/AppLoadingView.swift | 2 +- Swiftfin tvOS/Views/BasicAppSettingsView.swift | 2 +- .../Views/ChannelLibraryView/ChannelLibraryView.swift | 2 +- .../ChannelLibraryView/Components/WideChannelGridItem.swift | 2 +- .../ConnectToServerView/Components/LocalServerButton.swift | 2 +- .../Views/ConnectToServerView/ConnectToServerView.swift | 2 +- Swiftfin tvOS/Views/FontPickerView.swift | 2 +- .../HomeView/Components/CinematicRecentlyAddedView.swift | 2 +- .../Views/HomeView/Components/CinematicResumeItemView.swift | 2 +- .../Views/HomeView/Components/LatestInLibraryView.swift | 2 +- Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift | 2 +- .../Views/HomeView/Components/RecentlyAddedView.swift | 2 +- Swiftfin tvOS/Views/HomeView/HomeErrorView.swift | 2 +- Swiftfin tvOS/Views/HomeView/HomeView.swift | 2 +- Swiftfin tvOS/Views/ItemOverviewView.swift | 2 +- .../Views/ItemView/CinematicCollectionItemView.swift | 2 +- Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift | 2 +- Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift | 2 +- Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift | 2 +- Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift | 2 +- .../CollectionItemView/CollectionItemContentView.swift | 2 +- .../ItemView/CollectionItemView/CollectionItemView.swift | 2 +- .../Views/ItemView/Components/AboutView/AboutView.swift | 2 +- .../Components/AboutView/Components/AboutViewCard.swift | 2 +- .../Components/AboutView/Components/MediaSourcesCard.swift | 2 +- .../Components/AboutView/Components/OverviewCard.swift | 2 +- .../Components/AboutView/Components/RatingsCard.swift | 2 +- .../ItemView/Components/ActionButtons/ActionButton.swift | 2 +- .../Components/ActionButtons/ActionButtonHStack.swift | 2 +- .../ItemView/Components/ActionButtons/ActionMenu.swift | 2 +- .../Components/ActionButtons/RefreshMetadataButton.swift | 2 +- .../Views/ItemView/Components/AttributeHStack.swift | 2 +- .../Views/ItemView/Components/CastAndCrewHStack.swift | 2 +- .../Components/EpisodeSelector/Components/EpisodeCard.swift | 2 +- .../EpisodeSelector/Components/EpisodeContent.swift | 2 +- .../EpisodeSelector/Components/EpisodeHStack.swift | 2 +- .../Components/EpisodeSelector/Components/ErrorCard.swift | 2 +- .../Components/EpisodeSelector/Components/LoadingCard.swift | 2 +- .../Components/EpisodeSelector/EpisodeSelector.swift | 2 +- Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift | 2 +- .../Views/ItemView/Components/SimilarItemsHStack.swift | 2 +- .../Views/ItemView/Components/SpecialFeaturesHStack.swift | 2 +- .../ItemView/EpisodeItemView/EpisodeItemContentView.swift | 2 +- .../Views/ItemView/EpisodeItemView/EpisodeItemView.swift | 2 +- Swiftfin tvOS/Views/ItemView/ItemView.swift | 2 +- .../Views/ItemView/MovieItemView/MovieItemContentView.swift | 2 +- .../Views/ItemView/MovieItemView/MovieItemView.swift | 2 +- .../Views/ItemView/ScrollViews/CinematicScrollView.swift | 2 +- .../ItemView/SeriesItemView/SeriesItemContentView.swift | 2 +- .../Views/ItemView/SeriesItemView/SeriesItemView.swift | 2 +- Swiftfin tvOS/Views/LearnMoreModal.swift | 2 +- Swiftfin tvOS/Views/MediaSourceInfoView.swift | 2 +- Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift | 2 +- Swiftfin tvOS/Views/MediaView/MediaView.swift | 2 +- .../Views/PagingLibraryView/Components/LibraryRow.swift | 2 +- .../Views/PagingLibraryView/Components/ListRow.swift | 2 +- .../Views/PagingLibraryView/PagingLibraryView.swift | 2 +- .../ProgramsView/Components/ProgramButtonContent.swift | 2 +- .../ProgramsView/Components/ProgramProgressOverlay.swift | 2 +- Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift | 2 +- Swiftfin tvOS/Views/QuickConnectView.swift | 2 +- Swiftfin tvOS/Views/SearchView.swift | 2 +- .../Views/SelectUserView/Components/AddUserButton.swift | 2 +- .../SelectUserView/Components/SelectUserBottomBar.swift | 2 +- .../SelectUserView/Components/ServerSelectionMenu.swift | 2 +- .../Views/SelectUserView/Components/UserGridButton.swift | 2 +- Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift | 2 +- Swiftfin tvOS/Views/ServerDetailView.swift | 2 +- .../Components/CustomProfileButton.swift | 2 +- .../Components/EditCustomDeviceProfileView.swift | 2 +- .../CustomDeviceProfileSettingsView.swift | 2 +- .../Components/ListColumnsPickerView.swift | 2 +- .../Components/Sections/HomeSection.swift | 2 +- .../Components/Sections/ItemSection.swift | 2 +- .../CustomizeViewsSettings/CustomizeViewsSettings.swift | 2 +- .../Views/SettingsView/ExperimentalSettingsView.swift | 2 +- .../Views/SettingsView/IndicatorSettingsView.swift | 2 +- .../Views/SettingsView/PlaybackQualitySettingsView.swift | 2 +- Swiftfin tvOS/Views/SettingsView/SettingsView.swift | 2 +- .../Views/SettingsView/VideoPlayerSettingsView.swift | 2 +- .../Views/UserSignInView/Components/PublicUserButton.swift | 2 +- Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift | 2 +- .../Views/VideoPlayer/Components/LoadingView.swift | 2 +- Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift | 2 +- .../LiveOverlays/Components/LiveBottomBarView.swift | 2 +- .../Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift | 2 +- .../Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift | 2 +- .../Views/VideoPlayer/LiveOverlays/LiveOverlay.swift | 2 +- Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift | 2 +- Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift | 2 +- .../Views/VideoPlayer/Overlays/ChapterOverlay.swift | 2 +- .../Overlays/Components/ActionButtons/ActionButtons.swift | 2 +- .../Components/ActionButtons/AutoPlayActionButton.swift | 2 +- .../Components/ActionButtons/ChaptersActionButton.swift | 2 +- .../Components/ActionButtons/PlayNextItemActionButton.swift | 2 +- .../ActionButtons/PlayPreviousItemActionButton.swift | 2 +- .../Overlays/Components/ActionButtons/SubtitleButton.swift | 2 +- .../VideoPlayer/Overlays/Components/BarActionButtons.swift | 2 +- .../VideoPlayer/Overlays/Components/BottomBarView.swift | 2 +- .../Overlays/Components/tvOSSLider/SliderView.swift | 2 +- .../Overlays/Components/tvOSSLider/tvOSSlider.swift | 2 +- .../Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift | 2 +- Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift | 2 +- Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift | 2 +- .../Views/VideoPlayer/Overlays/SmallMenuOverlay.swift | 2 +- Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift | 2 +- Swiftfin/App/AppDelegate.swift | 2 +- Swiftfin/App/SwiftfinApp+ValueObservation.swift | 2 +- Swiftfin/App/SwiftfinApp.swift | 2 +- Swiftfin/Components/BasicStepper.swift | 2 +- Swiftfin/Components/CircularProgressView.swift | 2 +- Swiftfin/Components/CountryPicker.swift | 2 +- Swiftfin/Components/DelayedProgressView.swift | 2 +- Swiftfin/Components/DotHStack.swift | 2 +- Swiftfin/Components/ErrorView.swift | 2 +- Swiftfin/Components/GestureView.swift | 2 +- Swiftfin/Components/HourMinutePicker.swift | 2 +- Swiftfin/Components/LandscapePosterProgressBar.swift | 2 +- Swiftfin/Components/LanguagePicker.swift | 2 +- Swiftfin/Components/LearnMoreButton.swift | 2 +- .../LetterPickerBar/Components/LetterPickerButton.swift | 2 +- Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift | 2 +- Swiftfin/Components/ListRow.swift | 2 +- Swiftfin/Components/ListRowButton.swift | 2 +- Swiftfin/Components/ListTitleSection.swift | 2 +- .../NavigationBarFilterDrawer/FilterDrawerButton.swift | 2 +- .../NavigationBarFilterDrawer.swift | 2 +- Swiftfin/Components/OrderedSectionSelectorView.swift | 2 +- Swiftfin/Components/PillHStack.swift | 2 +- Swiftfin/Components/PosterButton.swift | 2 +- Swiftfin/Components/PosterHStack.swift | 2 +- Swiftfin/Components/PrimaryButton.swift | 2 +- Swiftfin/Components/SeeAllButton.swift | 2 +- Swiftfin/Components/SettingsBarButton.swift | 2 +- Swiftfin/Components/Slider/CapsuleSlider.swift | 2 +- Swiftfin/Components/Slider/Slider.swift | 2 +- Swiftfin/Components/Slider/ThumbSlider.swift | 2 +- Swiftfin/Components/SplitContentView.swift | 2 +- Swiftfin/Components/UnmaskSecureField.swift | 2 +- Swiftfin/Components/UpdateView.swift | 2 +- Swiftfin/Components/Video3DFormatPicker.swift | 2 +- Swiftfin/Components/iOS15View.swift | 2 +- Swiftfin/Extensions/ButtonStyle-iOS.swift | 2 +- Swiftfin/Extensions/Label-iOS.swift | 2 +- .../View/Modifiers/DetectOrientationModifier.swift | 2 +- .../View/Modifiers/NavigationBarCloseButton.swift | 2 +- .../NavigationBarDrawerModifier.swift | 2 +- .../NavigationBarDrawerView.swift | 2 +- .../Extensions/View/Modifiers/NavigationBarMenuButton.swift | 2 +- .../NavigationBarOffset/NavigationBarOffsetModifier.swift | 2 +- .../NavigationBarOffset/NavigationBarOffsetView.swift | 2 +- Swiftfin/Extensions/View/View-iOS.swift | 2 +- Swiftfin/Objects/AppURLHandler.swift | 2 +- Swiftfin/Objects/DeepLink.swift | 2 +- Swiftfin/Views/AboutAppView.swift | 2 +- .../Views/AdminDashboardView/APIKeyView/APIKeysView.swift | 2 +- .../APIKeyView/Components/APIKeysRow.swift | 2 +- .../ActiveSessionDetailView/ActiveSessionDetailView.swift | 2 +- .../ActiveSessionDetailView/Components/StreamSection.swift | 2 +- .../Components/TranscodeSection.swift | 2 +- .../ActiveSessionsView/ActiveSessionsView.swift | 2 +- .../Components/ActiveSessionProgressSection.swift | 2 +- .../ActiveSessionsView/Components/ActiveSessionRow.swift | 2 +- .../AddServerUserView/AddServerUserView.swift | 2 +- Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift | 2 +- .../Views/AdminDashboardView/Components/DeviceSection.swift | 2 +- .../Views/AdminDashboardView/Components/UserSection.swift | 2 +- .../Components/Sections/CompatibilitiesSection.swift | 2 +- .../Components/Sections/CustomDeviceNameSection.swift | 2 +- .../DeviceDetailsView/DeviceDetailsView.swift | 2 +- .../DevicesView/Components/DeviceRow.swift | 2 +- .../Views/AdminDashboardView/DevicesView/DevicesView.swift | 2 +- .../AdminDashboardView/ServerLogsView/ServerLogsView.swift | 2 +- .../ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift | 2 +- .../AddTaskTriggerView/Components/DayOfWeekRow.swift | 2 +- .../AddTaskTriggerView/Components/IntervalRow.swift | 2 +- .../AddTaskTriggerView/Components/TimeLimitSection.swift | 2 +- .../ServerTasks/AddTaskTriggerView/Components/TimeRow.swift | 2 +- .../AddTaskTriggerView/Components/TriggerTypeRow.swift | 2 +- .../Components/Sections/DetailsSection.swift | 2 +- .../Components/Sections/LastErrorSection.swift | 2 +- .../Components/Sections/LastRunSection.swift | 2 +- .../Components/Sections/ServerTaskProgressSection.swift | 2 +- .../Components/Sections/TriggersSection.swift | 2 +- .../EditServerTaskView/Components/TriggerRow.swift | 2 +- .../ServerTasks/EditServerTaskView/EditServerTaskView.swift | 2 +- .../ServerTasksView/Components/DestructiveServerTask.swift | 2 +- .../ServerTasksView/Components/ServerTaskRow.swift | 2 +- .../ServerTasks/ServerTasksView/ServerTasksView.swift | 2 +- .../AddAccessScheduleView/AddAccessScheduleView.swift | 2 +- .../Components/EditAccessScheduleRow.swift | 2 +- .../EditAccessScheduleView/EditAccessScheduleView.swift | 2 +- .../ServerUserAccessView/ServerUserAccessView.swift | 2 +- .../ServerUserDetailsView/ServerUserDetailsView.swift | 2 +- .../ServerUserDeviceAccessView.swift | 2 +- .../ServerUserLiveTVAccessView.swift | 2 +- .../ServerUserParentalRatingView.swift | 2 +- .../Components/Sections/ExternalAccessSection.swift | 2 +- .../Components/Sections/ManagementSection.swift | 2 +- .../Components/Sections/MediaPlaybackSection.swift | 2 +- .../Components/Sections/PermissionSection.swift | 2 +- .../Components/Sections/RemoteControlSection.swift | 2 +- .../Components/Sections/SessionsSection.swift | 2 +- .../Components/Sections/StatusSection.swift | 2 +- .../Components/Sections/SyncPlaySection.swift | 2 +- .../ServerUserPermissionsView.swift | 2 +- .../ServerUsersView/Components/ServerUsersRow.swift | 2 +- .../ServerUsersView/ServerUsersView.swift | 2 +- Swiftfin/Views/AppIconSelectorView.swift | 2 +- Swiftfin/Views/AppLoadingView.swift | 2 +- Swiftfin/Views/AppSettingsView/AppSettingsView.swift | 2 +- .../AppSettingsView/Components/SignOutIntervalSection.swift | 2 +- Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift | 2 +- .../ChannelLibraryView/Components/CompactChannelView.swift | 2 +- .../ChannelLibraryView/Components/DetailedChannelView.swift | 2 +- Swiftfin/Views/ConnectToServerView.swift | 2 +- Swiftfin/Views/DownloadListView.swift | 2 +- .../Views/DownloadTaskView/DownloadTaskContentView.swift | 2 +- Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift | 2 +- Swiftfin/Views/EditServerView.swift | 2 +- Swiftfin/Views/FilterView.swift | 2 +- Swiftfin/Views/FontPickerView.swift | 2 +- .../Views/HomeView/Components/ContinueWatchingView.swift | 2 +- .../Views/HomeView/Components/LatestInLibraryView.swift | 2 +- Swiftfin/Views/HomeView/Components/NextUpView.swift | 2 +- Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift | 2 +- Swiftfin/Views/HomeView/HomeView.swift | 2 +- .../ItemEditorView/Components/RefreshMetadataButton.swift | 2 +- .../IdentifyItemView/Components/RemoteSearchResultRow.swift | 2 +- .../Components/RemoteSearchResultView.swift | 2 +- .../ItemEditorView/IdentifyItemView/IdentifyItemView.swift | 2 +- Swiftfin/Views/ItemEditorView/ItemEditorView.swift | 2 +- .../AddItemElementView/AddItemElementView.swift | 2 +- .../AddItemElementView/Components/NameInput.swift | 2 +- .../Components/SearchResultsSection.swift | 2 +- .../EditItemElementView/Components/EditItemElementRow.swift | 2 +- .../EditItemElementView/EditItemElementView.swift | 2 +- .../ItemImages/AddItemImageView/AddItemImageView.swift | 2 +- .../ItemImages/EditItemImagesView/EditItemImagesView.swift | 2 +- .../AddItemElementView/AddItemElementView.swift | 2 +- .../AddItemElementView/Components/NameInput.swift | 2 +- .../Components/SearchResultsSection.swift | 2 +- .../EditItemElementView/Components/EditItemElementRow.swift | 2 +- .../EditItemElementView/EditItemElementView.swift | 2 +- .../EditMetadataView/Components/Sections/DateSection.swift | 2 +- .../Components/Sections/DisplayOrderSection.swift | 2 +- .../Components/Sections/EpisodeSection.swift | 2 +- .../Components/Sections/LocalizationSection.swift | 2 +- .../Components/Sections/LockMetadataSection.swift | 2 +- .../Components/Sections/MediaFormatSection.swift | 2 +- .../Components/Sections/OverviewSection.swift | 2 +- .../Components/Sections/ParentialRatingsSection.swift | 2 +- .../Components/Sections/ReviewsSection.swift | 2 +- .../Components/Sections/SeriesSection.swift | 2 +- .../EditMetadataView/Components/Sections/TitleSection.swift | 2 +- .../ItemMetadata/EditMetadataView/EditMetadataView.swift | 2 +- Swiftfin/Views/ItemOverviewView.swift | 2 +- .../Views/ItemView/Components/AboutView/AboutView.swift | 2 +- .../Components/AboutView/Components/AboutView+Card.swift | 2 +- .../Components/AboutView/Components/MediaSourcesCard.swift | 2 +- .../Components/AboutView/Components/OverviewCard.swift | 2 +- .../Components/AboutView/Components/RatingsCard.swift | 2 +- Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift | 2 +- Swiftfin/Views/ItemView/Components/AttributeHStack.swift | 2 +- Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift | 2 +- Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift | 2 +- .../Components/EpisodeSelector/Components/EmptyCard.swift | 2 +- .../Components/EpisodeSelector/Components/EpisodeCard.swift | 2 +- .../EpisodeSelector/Components/EpisodeContent.swift | 2 +- .../EpisodeSelector/Components/EpisodeHStack.swift | 2 +- .../Components/EpisodeSelector/Components/ErrorCard.swift | 2 +- .../Components/EpisodeSelector/Components/LoadingCard.swift | 2 +- .../Components/EpisodeSelector/EpisodeSelector.swift | 2 +- Swiftfin/Views/ItemView/Components/GenresHStack.swift | 2 +- Swiftfin/Views/ItemView/Components/OffsetScrollView.swift | 2 +- Swiftfin/Views/ItemView/Components/OverviewView.swift | 2 +- Swiftfin/Views/ItemView/Components/PlayButton.swift | 2 +- Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift | 2 +- .../Views/ItemView/Components/SpecialFeatureHStack.swift | 2 +- Swiftfin/Views/ItemView/Components/StudiosHStack.swift | 2 +- Swiftfin/Views/ItemView/ItemView.swift | 2 +- .../iOS/CollectionItemView/CollectionItemContentView.swift | 2 +- .../iOS/CollectionItemView/CollectionItemView.swift | 2 +- .../iOS/EpisodeItemView/EpisodeItemContentView.swift | 2 +- .../ItemView/iOS/EpisodeItemView/EpisodeItemView.swift | 2 +- .../ItemView/iOS/MovieItemView/MovieItemContentView.swift | 2 +- .../Views/ItemView/iOS/MovieItemView/MovieItemView.swift | 2 +- .../ItemView/iOS/ScrollViews/CinematicScrollView.swift | 2 +- .../ItemView/iOS/ScrollViews/CompactLogoScrollView.swift | 2 +- .../iOS/ScrollViews/CompactPortraitScrollView.swift | 2 +- .../ItemView/iOS/SeriesItemView/SeriesItemContentView.swift | 2 +- .../Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift | 2 +- .../iPadOSCollectionItemContentView.swift | 2 +- .../CollectionItemView/iPadOSCollectionItemView.swift | 2 +- .../iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift | 2 +- .../iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift | 2 +- .../iPadOS/MovieItemView/iPadOSMovieItemContentView.swift | 2 +- .../ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift | 2 +- .../iPadOS/ScrollViews/iPadOSCinematicScrollView.swift | 2 +- .../iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift | 2 +- .../iPadOS/SeriesItemView/iPadOSSeriesItemView.swift | 2 +- Swiftfin/Views/MediaSourceInfoView.swift | 2 +- Swiftfin/Views/MediaStreamInfoView.swift | 2 +- Swiftfin/Views/MediaView/Components/MediaItem.swift | 2 +- Swiftfin/Views/MediaView/MediaView.swift | 2 +- .../Views/PagingLibraryView/Components/LibraryRow.swift | 2 +- .../Components/LibraryViewTypeToggle.swift | 2 +- Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift | 2 +- .../ProgramsView/Components/ProgramButtonContent.swift | 2 +- .../ProgramsView/Components/ProgramProgressOverlay.swift | 2 +- Swiftfin/Views/ProgramsView/ProgramsView.swift | 2 +- Swiftfin/Views/QuickConnectView.swift | 2 +- .../Views/ResetUserPasswordView/ResetUserPasswordView.swift | 2 +- Swiftfin/Views/SearchView.swift | 2 +- .../Views/SelectUserView/Components/AddUserButton.swift | 2 +- Swiftfin/Views/SelectUserView/Components/AddUserRow.swift | 2 +- .../SelectUserView/Components/ServerSelectionMenu.swift | 2 +- .../Views/SelectUserView/Components/UserGridButton.swift | 2 +- Swiftfin/Views/SelectUserView/Components/UserRow.swift | 2 +- Swiftfin/Views/SelectUserView/SelectUserView.swift | 2 +- Swiftfin/Views/ServerCheckView.swift | 2 +- .../Components/CustomProfileButton.swift | 2 +- .../Components/EditCustomDeviceProfileView.swift | 2 +- .../CustomDeviceProfileSettingsView.swift | 2 +- .../Components/Sections/HomeSection.swift | 2 +- .../Components/Sections/ItemSection.swift | 2 +- .../CustomizeViewsSettings/CustomizeViewsSettings.swift | 2 +- Swiftfin/Views/SettingsView/DebugSettingsView.swift | 2 +- Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift | 2 +- Swiftfin/Views/SettingsView/GestureSettingsView.swift | 2 +- Swiftfin/Views/SettingsView/IndicatorSettingsView.swift | 2 +- .../Views/SettingsView/NativeVideoPlayerSettingsView.swift | 2 +- .../Views/SettingsView/PlaybackQualitySettingsView.swift | 2 +- .../SettingsView/Components/UserProfileRow.swift | 2 +- Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift | 2 +- .../UserProfileSettingsView/QuickConnectAuthorizeView.swift | 2 +- .../UserProfileSettingsView/UserLocalSecurityView.swift | 2 +- .../UserProfileSettingsView/UserProfileSettingsView.swift | 2 +- .../Components/ActionButtonSelectorView.swift | 2 +- .../Components/Sections/ButtonSection.swift | 2 +- .../Components/Sections/SliderSection.swift | 2 +- .../Components/Sections/SubtitleSection.swift | 2 +- .../Components/Sections/TimestampSection.swift | 2 +- .../Components/Sections/TransitionSection.swift | 2 +- .../VideoPlayerSettingsView/VideoPlayerSettingsView.swift | 2 +- .../UserProfileImagePicker/Components/PhotoPicker.swift | 2 +- .../Components/SquareImageCropView.swift | 2 +- .../UserProfileImagePicker/UserProfileImagePicker.swift | 2 +- .../Views/UserSignInView/Components/PublicUserRow.swift | 2 +- .../UserSignInView/Components/UserSignInSecurityView.swift | 2 +- Swiftfin/Views/UserSignInView/UserSignInView.swift | 2 +- Swiftfin/Views/VideoPlayer/Components/LoadingView.swift | 2 +- .../Views/VideoPlayer/Components/PlaybackSettingsView.swift | 2 +- Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift | 2 +- .../LiveOverlays/Components/LiveBottomBarView.swift | 2 +- .../LiveOverlays/Components/LiveTopBarView.swift | 2 +- .../PlaybackButtons/LiveLargePlaybackButtons.swift | 2 +- .../PlaybackButtons/LiveSmallPlaybackButton.swift | 2 +- .../Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift | 2 +- Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift | 2 +- Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift | 2 +- Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift | 2 +- Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift | 2 +- .../Overlays/Components/ActionButtons/ActionButtons.swift | 2 +- .../Components/ActionButtons/AdvancedActionButton.swift | 2 +- .../Components/ActionButtons/AspectFillActionButton.swift | 2 +- .../Components/ActionButtons/AudioActionButton.swift | 2 +- .../Components/ActionButtons/AutoPlayActionButton.swift | 2 +- .../Components/ActionButtons/ChaptersActionButton.swift | 2 +- .../Components/ActionButtons/PlayNextItemActionButton.swift | 2 +- .../ActionButtons/PlayPreviousItemActionButton.swift | 2 +- .../ActionButtons/PlaybackSpeedActionButton.swift | 2 +- .../Components/ActionButtons/SubtitleActionButton.swift | 2 +- .../VideoPlayer/Overlays/Components/BarActionButtons.swift | 2 +- .../VideoPlayer/Overlays/Components/BottomBarView.swift | 2 +- .../VideoPlayer/Overlays/Components/ChapterTrack.swift | 2 +- .../Views/VideoPlayer/Overlays/Components/OverlayMenu.swift | 2 +- .../Components/PlaybackButtons/LargePlaybackButtons.swift | 2 +- .../Components/PlaybackButtons/SmallPlaybackButtons.swift | 2 +- .../Overlays/Components/Timestamp/CompactTimeStamp.swift | 2 +- .../Overlays/Components/Timestamp/SplitTimestamp.swift | 2 +- .../Views/VideoPlayer/Overlays/Components/TopBarView.swift | 2 +- Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift | 2 +- Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift | 2 +- Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift | 2 +- Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift | 2 +- Swiftfin/Views/VideoPlayer/VideoPlayer.swift | 2 +- 747 files changed, 746 insertions(+), 752 deletions(-) diff --git a/PreferencesView/Sources/PreferencesView/Box.swift b/PreferencesView/Sources/PreferencesView/Box.swift index dc054e087..99f822a1c 100644 --- a/PreferencesView/Sources/PreferencesView/Box.swift +++ b/PreferencesView/Sources/PreferencesView/Box.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // class Box { diff --git a/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift b/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift index 1227f481a..484d4a73f 100644 --- a/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift +++ b/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift b/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift index f287d5fa2..41da3f77b 100644 --- a/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift +++ b/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift b/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift index a19e44797..16b4150c6 100644 --- a/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift +++ b/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/PreferencesView.swift b/PreferencesView/Sources/PreferencesView/PreferencesView.swift index 2ed71ce90..031180aaa 100644 --- a/PreferencesView/Sources/PreferencesView/PreferencesView.swift +++ b/PreferencesView/Sources/PreferencesView/PreferencesView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/PressCommandAction.swift b/PreferencesView/Sources/PreferencesView/PressCommandAction.swift index f9e5af83f..98d028ba7 100644 --- a/PreferencesView/Sources/PreferencesView/PressCommandAction.swift +++ b/PreferencesView/Sources/PreferencesView/PressCommandAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift b/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift index 5d67c31c9..ddc7d2b97 100644 --- a/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift +++ b/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift b/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift index abe471a97..8bc6bac58 100644 --- a/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift +++ b/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift b/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift index b35c85c38..b4f53b3bc 100644 --- a/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift +++ b/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwizzleSwift diff --git a/PreferencesView/Sources/PreferencesView/ViewExtensions.swift b/PreferencesView/Sources/PreferencesView/ViewExtensions.swift index 39ce78302..33e434f94 100644 --- a/PreferencesView/Sources/PreferencesView/ViewExtensions.swift +++ b/PreferencesView/Sources/PreferencesView/ViewExtensions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/RedrawOnNotificationView.swift b/RedrawOnNotificationView.swift index fb0cda448..1f1881af0 100644 --- a/RedrawOnNotificationView.swift +++ b/RedrawOnNotificationView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Scripts/Translations/AlphabetizeStrings.swift b/Scripts/Translations/AlphabetizeStrings.swift index ffd94657a..bacdb0b48 100644 --- a/Scripts/Translations/AlphabetizeStrings.swift +++ b/Scripts/Translations/AlphabetizeStrings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Scripts/Translations/PurgeUnusedStrings.swift b/Scripts/Translations/PurgeUnusedStrings.swift index cddfbadc3..ff9c9cc3d 100755 --- a/Scripts/Translations/PurgeUnusedStrings.swift +++ b/Scripts/Translations/PurgeUnusedStrings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/AppIcons.swift b/Shared/AppIcons/AppIcons.swift index 743fc9873..847d22756 100644 --- a/Shared/AppIcons/AppIcons.swift +++ b/Shared/AppIcons/AppIcons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/DarkAppIcon.swift b/Shared/AppIcons/DarkAppIcon.swift index 443ce9b92..5192e9ef8 100644 --- a/Shared/AppIcons/DarkAppIcon.swift +++ b/Shared/AppIcons/DarkAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/InvertedDarkAppIcon.swift b/Shared/AppIcons/InvertedDarkAppIcon.swift index 20c3b1802..70ec89094 100644 --- a/Shared/AppIcons/InvertedDarkAppIcon.swift +++ b/Shared/AppIcons/InvertedDarkAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/InvertedLightAppIcon.swift b/Shared/AppIcons/InvertedLightAppIcon.swift index d9ca8fd70..3dacdfbbb 100644 --- a/Shared/AppIcons/InvertedLightAppIcon.swift +++ b/Shared/AppIcons/InvertedLightAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/LightAppIcon.swift b/Shared/AppIcons/LightAppIcon.swift index 02dc79de6..8a5b6244a 100644 --- a/Shared/AppIcons/LightAppIcon.swift +++ b/Shared/AppIcons/LightAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/PrimaryAppIcon.swift b/Shared/AppIcons/PrimaryAppIcon.swift index e0c0ca5f8..45de9b8f2 100644 --- a/Shared/AppIcons/PrimaryAppIcon.swift +++ b/Shared/AppIcons/PrimaryAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Components/AlternateLayoutView.swift b/Shared/Components/AlternateLayoutView.swift index 6f0e2608c..ef3da0c41 100644 --- a/Shared/Components/AlternateLayoutView.swift +++ b/Shared/Components/AlternateLayoutView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/AssertionFailureView.swift b/Shared/Components/AssertionFailureView.swift index b98205315..52f1e2935 100644 --- a/Shared/Components/AssertionFailureView.swift +++ b/Shared/Components/AssertionFailureView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/BlurView.swift b/Shared/Components/BlurView.swift index 5809525ad..e428860eb 100644 --- a/Shared/Components/BlurView.swift +++ b/Shared/Components/BlurView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/BulletedList.swift b/Shared/Components/BulletedList.swift index 8a760a085..8d3220cd4 100644 --- a/Shared/Components/BulletedList.swift +++ b/Shared/Components/BulletedList.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/ChevronAlertButton.swift b/Shared/Components/ChevronAlertButton.swift index 91b6d0dc1..dd157b7f7 100644 --- a/Shared/Components/ChevronAlertButton.swift +++ b/Shared/Components/ChevronAlertButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/ChevronButton.swift b/Shared/Components/ChevronButton.swift index 46be21aa1..c5fb3d3a9 100644 --- a/Shared/Components/ChevronButton.swift +++ b/Shared/Components/ChevronButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/FastSVGView.swift b/Shared/Components/FastSVGView.swift index 3a4c2f180..866f77def 100644 --- a/Shared/Components/FastSVGView.swift +++ b/Shared/Components/FastSVGView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SVGKit diff --git a/Shared/Components/ImageView.swift b/Shared/Components/ImageView.swift index 11eda88de..793d2db5a 100644 --- a/Shared/Components/ImageView.swift +++ b/Shared/Components/ImageView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Shared/Components/LetterPickerOrientation.swift b/Shared/Components/LetterPickerOrientation.swift index c5e55ecb6..0a7f9aa47 100644 --- a/Shared/Components/LetterPickerOrientation.swift +++ b/Shared/Components/LetterPickerOrientation.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/ListRowCheckbox.swift b/Shared/Components/ListRowCheckbox.swift index 01b01a70a..755b8ce83 100644 --- a/Shared/Components/ListRowCheckbox.swift +++ b/Shared/Components/ListRowCheckbox.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/MaxHeightText.swift b/Shared/Components/MaxHeightText.swift index a31126858..772c748a7 100644 --- a/Shared/Components/MaxHeightText.swift +++ b/Shared/Components/MaxHeightText.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/PosterIndicators/FavoriteIndicator.swift b/Shared/Components/PosterIndicators/FavoriteIndicator.swift index 0121117ad..7e9f67ce6 100644 --- a/Shared/Components/PosterIndicators/FavoriteIndicator.swift +++ b/Shared/Components/PosterIndicators/FavoriteIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/PosterIndicators/ProgressIndicator.swift b/Shared/Components/PosterIndicators/ProgressIndicator.swift index d01857f54..09477c30c 100644 --- a/Shared/Components/PosterIndicators/ProgressIndicator.swift +++ b/Shared/Components/PosterIndicators/ProgressIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/PosterIndicators/UnwatchedIndicator.swift b/Shared/Components/PosterIndicators/UnwatchedIndicator.swift index b8838cc9c..9d0abfc26 100644 --- a/Shared/Components/PosterIndicators/UnwatchedIndicator.swift +++ b/Shared/Components/PosterIndicators/UnwatchedIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/PosterIndicators/WatchedIndicator.swift b/Shared/Components/PosterIndicators/WatchedIndicator.swift index 1efd10271..8744feb22 100644 --- a/Shared/Components/PosterIndicators/WatchedIndicator.swift +++ b/Shared/Components/PosterIndicators/WatchedIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/ProgressBar.swift b/Shared/Components/ProgressBar.swift index 5b505e7cb..b0b1816aa 100644 --- a/Shared/Components/ProgressBar.swift +++ b/Shared/Components/ProgressBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/RotateContentView.swift b/Shared/Components/RotateContentView.swift index 2ab74fcc1..8dd8625cb 100644 --- a/Shared/Components/RotateContentView.swift +++ b/Shared/Components/RotateContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/RowDivider.swift b/Shared/Components/RowDivider.swift index 50e62ca45..0d58f07fd 100644 --- a/Shared/Components/RowDivider.swift +++ b/Shared/Components/RowDivider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/SelectorView.swift b/Shared/Components/SelectorView.swift index a8c7666d2..ba7d2a2d8 100644 --- a/Shared/Components/SelectorView.swift +++ b/Shared/Components/SelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/SeparatorHStack.swift b/Shared/Components/SeparatorHStack.swift index c9b732ced..500227418 100644 --- a/Shared/Components/SeparatorHStack.swift +++ b/Shared/Components/SeparatorHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/SeparatorVStack.swift b/Shared/Components/SeparatorVStack.swift index 44721f997..b7db46b2c 100644 --- a/Shared/Components/SeparatorVStack.swift +++ b/Shared/Components/SeparatorVStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/SystemImageContentView.swift b/Shared/Components/SystemImageContentView.swift index dcf237ccf..b32c9eb6d 100644 --- a/Shared/Components/SystemImageContentView.swift +++ b/Shared/Components/SystemImageContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/TextPairView.swift b/Shared/Components/TextPairView.swift index 66c9ace9f..284063f3e 100644 --- a/Shared/Components/TextPairView.swift +++ b/Shared/Components/TextPairView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/TruncatedText.swift b/Shared/Components/TruncatedText.swift index a5ba38c50..d042a9239 100644 --- a/Shared/Components/TruncatedText.swift +++ b/Shared/Components/TruncatedText.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/UserProfileImage/UserProfileHeroImage.swift b/Shared/Components/UserProfileImage/UserProfileHeroImage.swift index 01352720f..15d1b4fba 100644 --- a/Shared/Components/UserProfileImage/UserProfileHeroImage.swift +++ b/Shared/Components/UserProfileImage/UserProfileHeroImage.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/UserProfileImage/UserProfileImage.swift b/Shared/Components/UserProfileImage/UserProfileImage.swift index aab0aa755..eeabb7e72 100644 --- a/Shared/Components/UserProfileImage/UserProfileImage.swift +++ b/Shared/Components/UserProfileImage/UserProfileImage.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/WrappedView.swift b/Shared/Components/WrappedView.swift index eaca6e7e2..38d78f23d 100644 --- a/Shared/Components/WrappedView.swift +++ b/Shared/Components/WrappedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Coordinators/AdminDashboardCoordinator.swift b/Shared/Coordinators/AdminDashboardCoordinator.swift index 4dfcecb9d..5c7705687 100644 --- a/Shared/Coordinators/AdminDashboardCoordinator.swift +++ b/Shared/Coordinators/AdminDashboardCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Coordinators/AppSettingsCoordinator.swift b/Shared/Coordinators/AppSettingsCoordinator.swift index 8cc419a79..3c897e21c 100644 --- a/Shared/Coordinators/AppSettingsCoordinator.swift +++ b/Shared/Coordinators/AppSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import PulseUI diff --git a/Shared/Coordinators/BasicNavigationCoordinator.swift b/Shared/Coordinators/BasicNavigationCoordinator.swift index b3093cc75..86d00749e 100644 --- a/Shared/Coordinators/BasicNavigationCoordinator.swift +++ b/Shared/Coordinators/BasicNavigationCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/CustomDeviceProfileCoordinator.swift b/Shared/Coordinators/CustomDeviceProfileCoordinator.swift index 7af5a547a..998e110c5 100644 --- a/Shared/Coordinators/CustomDeviceProfileCoordinator.swift +++ b/Shared/Coordinators/CustomDeviceProfileCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/CustomizeSettingsCoordinator.swift b/Shared/Coordinators/CustomizeSettingsCoordinator.swift index 259bd0334..91cc4869a 100644 --- a/Shared/Coordinators/CustomizeSettingsCoordinator.swift +++ b/Shared/Coordinators/CustomizeSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/DownloadListCoordinator.swift b/Shared/Coordinators/DownloadListCoordinator.swift index fc9af3622..6706f84b0 100644 --- a/Shared/Coordinators/DownloadListCoordinator.swift +++ b/Shared/Coordinators/DownloadListCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // #if os(iOS) diff --git a/Shared/Coordinators/DownloadTaskCoordinator.swift b/Shared/Coordinators/DownloadTaskCoordinator.swift index 6a9b72bc0..b3eaa133d 100644 --- a/Shared/Coordinators/DownloadTaskCoordinator.swift +++ b/Shared/Coordinators/DownloadTaskCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // #if os(iOS) diff --git a/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift b/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift index f84149b5f..c414e2411 100644 --- a/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift +++ b/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/FilterCoordinator.swift b/Shared/Coordinators/FilterCoordinator.swift index be8c8fae0..851f57891 100644 --- a/Shared/Coordinators/FilterCoordinator.swift +++ b/Shared/Coordinators/FilterCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index d4fc91f48..aba2cbe8b 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift index ff902321e..9a68e28a3 100644 --- a/Shared/Coordinators/ItemCoordinator.swift +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index eb4a5f18b..faeb4963e 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index 950263f25..0b708424f 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift b/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift index 12ccd6fe6..fb5125e48 100644 --- a/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift +++ b/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift b/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift index 1e091649c..294146d28 100644 --- a/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift +++ b/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/LiveVideoPlayerCoordinator.swift b/Shared/Coordinators/LiveVideoPlayerCoordinator.swift index 16c449ec8..cda151b2b 100644 --- a/Shared/Coordinators/LiveVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/LiveVideoPlayerCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift index 71c868c7f..d5566c06b 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift index 39d3f9b24..048b3e8e3 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift index 2e93413d3..d23fc771a 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index 3e9f28fe4..255af5158 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/MediaCoordinator.swift b/Shared/Coordinators/MediaCoordinator.swift index dcf178a09..83d0280df 100644 --- a/Shared/Coordinators/MediaCoordinator.swift +++ b/Shared/Coordinators/MediaCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/MediaSourceInfoCoordinator.swift b/Shared/Coordinators/MediaSourceInfoCoordinator.swift index b1d0df0a5..34bc36dda 100644 --- a/Shared/Coordinators/MediaSourceInfoCoordinator.swift +++ b/Shared/Coordinators/MediaSourceInfoCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift b/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift index 88d388b6e..2cc4d9d8c 100644 --- a/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift +++ b/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/PlaybackSettingsCoordinator.swift b/Shared/Coordinators/PlaybackSettingsCoordinator.swift index 28015f185..5c0b4cb5a 100644 --- a/Shared/Coordinators/PlaybackSettingsCoordinator.swift +++ b/Shared/Coordinators/PlaybackSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Coordinators/SearchCoordinator.swift b/Shared/Coordinators/SearchCoordinator.swift index 4f6f1e3f0..2e03217ae 100644 --- a/Shared/Coordinators/SearchCoordinator.swift +++ b/Shared/Coordinators/SearchCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/SelectUserCoordinator.swift b/Shared/Coordinators/SelectUserCoordinator.swift index d12d11983..fc0e5459d 100644 --- a/Shared/Coordinators/SelectUserCoordinator.swift +++ b/Shared/Coordinators/SelectUserCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index e4517738d..00c566f01 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Coordinators/UserProfileImageCoordinator.swift b/Shared/Coordinators/UserProfileImageCoordinator.swift index 3cbe0a51a..d9b217839 100644 --- a/Shared/Coordinators/UserProfileImageCoordinator.swift +++ b/Shared/Coordinators/UserProfileImageCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/UserSignInCoordinator.swift b/Shared/Coordinators/UserSignInCoordinator.swift index adf88edc0..ecbbb3338 100644 --- a/Shared/Coordinators/UserSignInCoordinator.swift +++ b/Shared/Coordinators/UserSignInCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/VideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator.swift index b5dcb37df..59fa79999 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift b/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift index 0133f055b..5ed3b9100 100644 --- a/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift b/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift index 4ca46fe70..8d04118b7 100644 --- a/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Errors/NetworkError.swift b/Shared/Errors/NetworkError.swift index 8644e4529..9a7384586 100644 --- a/Shared/Errors/NetworkError.swift +++ b/Shared/Errors/NetworkError.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Array.swift b/Shared/Extensions/Array.swift index 202b471b8..3ec15df30 100644 --- a/Shared/Extensions/Array.swift +++ b/Shared/Extensions/Array.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Binding.swift b/Shared/Extensions/Binding.swift index 06a00e359..c698e748b 100644 --- a/Shared/Extensions/Binding.swift +++ b/Shared/Extensions/Binding.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Button.swift b/Shared/Extensions/Button.swift index 8e9358a4f..6428120be 100644 --- a/Shared/Extensions/Button.swift +++ b/Shared/Extensions/Button.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/CGPoint.swift b/Shared/Extensions/CGPoint.swift index e7de1d28e..6685e385e 100644 --- a/Shared/Extensions/CGPoint.swift +++ b/Shared/Extensions/CGPoint.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/CGSize.swift b/Shared/Extensions/CGSize.swift index fa2e9a3a7..68ce49b44 100644 --- a/Shared/Extensions/CGSize.swift +++ b/Shared/Extensions/CGSize.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/Collection.swift b/Shared/Extensions/Collection.swift index c56236a57..68089011e 100644 --- a/Shared/Extensions/Collection.swift +++ b/Shared/Extensions/Collection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Color.swift b/Shared/Extensions/Color.swift index 16bc9b311..49b865ee5 100644 --- a/Shared/Extensions/Color.swift +++ b/Shared/Extensions/Color.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/CoreStore.swift b/Shared/Extensions/CoreStore.swift index 6e5d27c9d..a8d2d4d38 100644 --- a/Shared/Extensions/CoreStore.swift +++ b/Shared/Extensions/CoreStore.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/Extensions/Dictionary.swift b/Shared/Extensions/Dictionary.swift index 7364e2766..710194807 100644 --- a/Shared/Extensions/Dictionary.swift +++ b/Shared/Extensions/Dictionary.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Double.swift b/Shared/Extensions/Double.swift index 7840f9271..bdeb29c10 100644 --- a/Shared/Extensions/Double.swift +++ b/Shared/Extensions/Double.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Edge.swift b/Shared/Extensions/Edge.swift index bc505bf4d..1c6915332 100644 --- a/Shared/Extensions/Edge.swift +++ b/Shared/Extensions/Edge.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/EdgeInsets.swift b/Shared/Extensions/EdgeInsets.swift index 65a2a671d..3118d74a5 100644 --- a/Shared/Extensions/EdgeInsets.swift +++ b/Shared/Extensions/EdgeInsets.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift index f39ae4533..11d15b73e 100644 --- a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift +++ b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift index 6a0df59ce..bb197f892 100644 --- a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift +++ b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Equatable.swift b/Shared/Extensions/Equatable.swift index 23ce2b585..25925e3ea 100644 --- a/Shared/Extensions/Equatable.swift +++ b/Shared/Extensions/Equatable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Files.swift b/Shared/Extensions/Files.swift index 58e320e4b..d7f2dc77c 100644 --- a/Shared/Extensions/Files.swift +++ b/Shared/Extensions/Files.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Font.swift b/Shared/Extensions/Font.swift index 0338ef117..a27d6911d 100644 --- a/Shared/Extensions/Font.swift +++ b/Shared/Extensions/Font.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/FormatStyle.swift b/Shared/Extensions/FormatStyle.swift index 95652cdc9..78fa3bc69 100644 --- a/Shared/Extensions/FormatStyle.swift +++ b/Shared/Extensions/FormatStyle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Hashable.swift b/Shared/Extensions/Hashable.swift index 13970de47..49bbb6a6d 100644 --- a/Shared/Extensions/Hashable.swift +++ b/Shared/Extensions/Hashable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/HorizontalAlignment.swift b/Shared/Extensions/HorizontalAlignment.swift index 3cce5dd34..d9aa92a04 100644 --- a/Shared/Extensions/HorizontalAlignment.swift +++ b/Shared/Extensions/HorizontalAlignment.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Int.swift b/Shared/Extensions/Int.swift index 227c9e8cf..a0942a732 100644 --- a/Shared/Extensions/Int.swift +++ b/Shared/Extensions/Int.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift b/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift index 1506ac82e..c7b2469e2 100644 --- a/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift +++ b/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift index cef5fef00..b3225015a 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift index 18e56f43a..03c20900d 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift index e4271a954..30294d02d 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift index 98c541abe..0f0d267fb 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Algorithms diff --git a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift index 3c7c7044b..df5c47a12 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift index f21d27bb9..7d93f69fc 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift index 86de41999..a044d06d2 100644 --- a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/CodecProfile.swift b/Shared/Extensions/JellyfinAPI/CodecProfile.swift index b67674d96..c70de069c 100644 --- a/Shared/Extensions/JellyfinAPI/CodecProfile.swift +++ b/Shared/Extensions/JellyfinAPI/CodecProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DayOfWeek.swift b/Shared/Extensions/JellyfinAPI/DayOfWeek.swift index 75bfb3195..0959e1b9b 100644 --- a/Shared/Extensions/JellyfinAPI/DayOfWeek.swift +++ b/Shared/Extensions/JellyfinAPI/DayOfWeek.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DeviceInfo.swift b/Shared/Extensions/JellyfinAPI/DeviceInfo.swift index 9876bc15d..0dd2b9f2c 100644 --- a/Shared/Extensions/JellyfinAPI/DeviceInfo.swift +++ b/Shared/Extensions/JellyfinAPI/DeviceInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DeviceProfile.swift b/Shared/Extensions/JellyfinAPI/DeviceProfile.swift index df1ab0db5..8636feec2 100644 --- a/Shared/Extensions/JellyfinAPI/DeviceProfile.swift +++ b/Shared/Extensions/JellyfinAPI/DeviceProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/DeviceType.swift b/Shared/Extensions/JellyfinAPI/DeviceType.swift index 48a94ce45..143acbf82 100644 --- a/Shared/Extensions/JellyfinAPI/DeviceType.swift +++ b/Shared/Extensions/JellyfinAPI/DeviceType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift b/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift index 2c4c1d838..cbae5ec4e 100644 --- a/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift +++ b/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift b/Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift index 62735b664..9cb3c6a02 100644 --- a/Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift +++ b/Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift b/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift index 63a6d99ed..b27087f96 100644 --- a/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift +++ b/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ImageInfo.swift b/Shared/Extensions/JellyfinAPI/ImageInfo.swift index 50f832ff8..1560e7df2 100644 --- a/Shared/Extensions/JellyfinAPI/ImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ImageInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ItemFields.swift b/Shared/Extensions/JellyfinAPI/ItemFields.swift index 1e299f98e..974df1873 100644 --- a/Shared/Extensions/JellyfinAPI/ItemFields.swift +++ b/Shared/Extensions/JellyfinAPI/ItemFields.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift b/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift index f9a32404d..d234dad9f 100644 --- a/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift +++ b/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift b/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift index 0e8eb714c..6b7ababac 100644 --- a/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift +++ b/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/JellyfinClient.swift b/Shared/Extensions/JellyfinAPI/JellyfinClient.swift index bd238dc70..38155236c 100644 --- a/Shared/Extensions/JellyfinAPI/JellyfinClient.swift +++ b/Shared/Extensions/JellyfinAPI/JellyfinClient.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift b/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift index f8354c758..728163db8 100644 --- a/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift +++ b/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift b/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift index eb6435172..5988a749f 100644 --- a/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift +++ b/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift index 15ff3815f..0f0c321dd 100644 --- a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift index d237c8821..117c55b1a 100644 --- a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift +++ b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/MediaStream.swift b/Shared/Extensions/JellyfinAPI/MediaStream.swift index 7b702acf9..c9652a6a4 100644 --- a/Shared/Extensions/JellyfinAPI/MediaStream.swift +++ b/Shared/Extensions/JellyfinAPI/MediaStream.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Extensions/JellyfinAPI/MetadataField.swift b/Shared/Extensions/JellyfinAPI/MetadataField.swift index a1c719072..fcee7c435 100644 --- a/Shared/Extensions/JellyfinAPI/MetadataField.swift +++ b/Shared/Extensions/JellyfinAPI/MetadataField.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/NameGuidPair.swift b/Shared/Extensions/JellyfinAPI/NameGuidPair.swift index f9b81bbfd..c5c32a6ae 100644 --- a/Shared/Extensions/JellyfinAPI/NameGuidPair.swift +++ b/Shared/Extensions/JellyfinAPI/NameGuidPair.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ParentalRating.swift b/Shared/Extensions/JellyfinAPI/ParentalRating.swift index dc57c7120..43ce6a667 100644 --- a/Shared/Extensions/JellyfinAPI/ParentalRating.swift +++ b/Shared/Extensions/JellyfinAPI/ParentalRating.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/PersonKind.swift b/Shared/Extensions/JellyfinAPI/PersonKind.swift index bdd953118..243fa8a74 100644 --- a/Shared/Extensions/JellyfinAPI/PersonKind.swift +++ b/Shared/Extensions/JellyfinAPI/PersonKind.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/PlayMethod.swift b/Shared/Extensions/JellyfinAPI/PlayMethod.swift index f18618c40..6fa9c3ef4 100644 --- a/Shared/Extensions/JellyfinAPI/PlayMethod.swift +++ b/Shared/Extensions/JellyfinAPI/PlayMethod.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift b/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift index f0058a7c7..c33d5e172 100644 --- a/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift +++ b/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift index 467ddd8fe..4d20aef40 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift index 9ea81b501..b1c05efcb 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ServerTicks.swift b/Shared/Extensions/JellyfinAPI/ServerTicks.swift index 22aecfa74..6570f3746 100644 --- a/Shared/Extensions/JellyfinAPI/ServerTicks.swift +++ b/Shared/Extensions/JellyfinAPI/ServerTicks.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SessionInfo.swift b/Shared/Extensions/JellyfinAPI/SessionInfo.swift index 6c50f6afb..bf6495825 100644 --- a/Shared/Extensions/JellyfinAPI/SessionInfo.swift +++ b/Shared/Extensions/JellyfinAPI/SessionInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift b/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift index c68e659a2..32b3d7705 100644 --- a/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift +++ b/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift b/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift index 483db9880..6ee865cd0 100644 --- a/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift +++ b/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift b/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift index 6f124efc4..fd8ec3bc1 100644 --- a/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift +++ b/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift b/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift index 9e11c93e7..0537698a4 100644 --- a/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift +++ b/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift b/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift index a48b0777c..25046f0e9 100644 --- a/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift +++ b/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TaskState.swift b/Shared/Extensions/JellyfinAPI/TaskState.swift index 50ba73b90..06f63a6a4 100644 --- a/Shared/Extensions/JellyfinAPI/TaskState.swift +++ b/Shared/Extensions/JellyfinAPI/TaskState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift b/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift index add2d13cf..1141fa55f 100644 --- a/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift +++ b/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TranscodeReason.swift b/Shared/Extensions/JellyfinAPI/TranscodeReason.swift index 6049e2efd..365831183 100644 --- a/Shared/Extensions/JellyfinAPI/TranscodeReason.swift +++ b/Shared/Extensions/JellyfinAPI/TranscodeReason.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift b/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift index 699b69ab8..39edbea68 100644 --- a/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift +++ b/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/UserDto.swift b/Shared/Extensions/JellyfinAPI/UserDto.swift index 59de3902e..61404ace8 100644 --- a/Shared/Extensions/JellyfinAPI/UserDto.swift +++ b/Shared/Extensions/JellyfinAPI/UserDto.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/Video3DFormat.swift b/Shared/Extensions/JellyfinAPI/Video3DFormat.swift index f8073b14f..662152a61 100644 --- a/Shared/Extensions/JellyfinAPI/Video3DFormat.swift +++ b/Shared/Extensions/JellyfinAPI/Video3DFormat.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/NavigationCoordinatable.swift b/Shared/Extensions/NavigationCoordinatable.swift index dd327f7ea..276ca9f36 100644 --- a/Shared/Extensions/NavigationCoordinatable.swift +++ b/Shared/Extensions/NavigationCoordinatable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Extensions/Nuke/DataCache.swift b/Shared/Extensions/Nuke/DataCache.swift index c3eff914f..a57bd54e1 100644 --- a/Shared/Extensions/Nuke/DataCache.swift +++ b/Shared/Extensions/Nuke/DataCache.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/Extensions/Nuke/ImagePipeline.swift b/Shared/Extensions/Nuke/ImagePipeline.swift index fb4faff97..68a544687 100644 --- a/Shared/Extensions/Nuke/ImagePipeline.swift +++ b/Shared/Extensions/Nuke/ImagePipeline.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Optional.swift b/Shared/Extensions/Optional.swift index 82234f9a5..1ad51f827 100644 --- a/Shared/Extensions/Optional.swift +++ b/Shared/Extensions/Optional.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/OrderedDictionary.swift b/Shared/Extensions/OrderedDictionary.swift index 302c1326d..13394084b 100644 --- a/Shared/Extensions/OrderedDictionary.swift +++ b/Shared/Extensions/OrderedDictionary.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections diff --git a/Shared/Extensions/PersistentLogHandler.swift b/Shared/Extensions/PersistentLogHandler.swift index 5b1d76e97..f3cdf3dea 100644 --- a/Shared/Extensions/PersistentLogHandler.swift +++ b/Shared/Extensions/PersistentLogHandler.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Sequence.swift b/Shared/Extensions/Sequence.swift index afc8fee82..525adbc0b 100644 --- a/Shared/Extensions/Sequence.swift +++ b/Shared/Extensions/Sequence.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Set.swift b/Shared/Extensions/Set.swift index a44224603..f1786d8f8 100644 --- a/Shared/Extensions/Set.swift +++ b/Shared/Extensions/Set.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/String.swift b/Shared/Extensions/String.swift index cffd79c2e..80aba5400 100644 --- a/Shared/Extensions/String.swift +++ b/Shared/Extensions/String.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Algorithms diff --git a/Shared/Extensions/Task.swift b/Shared/Extensions/Task.swift index cff9cd90a..7e7302aa1 100644 --- a/Shared/Extensions/Task.swift +++ b/Shared/Extensions/Task.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Extensions/Text.swift b/Shared/Extensions/Text.swift index c49808884..c376aca59 100644 --- a/Shared/Extensions/Text.swift +++ b/Shared/Extensions/Text.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/UIApplication.swift b/Shared/Extensions/UIApplication.swift index fd83c1306..2cac26023 100644 --- a/Shared/Extensions/UIApplication.swift +++ b/Shared/Extensions/UIApplication.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/UIColor.swift b/Shared/Extensions/UIColor.swift index a41a6f4ea..f8b78a9ae 100644 --- a/Shared/Extensions/UIColor.swift +++ b/Shared/Extensions/UIColor.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/UIDevice.swift b/Shared/Extensions/UIDevice.swift index 2668ebcff..a8685f3c6 100644 --- a/Shared/Extensions/UIDevice.swift +++ b/Shared/Extensions/UIDevice.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/UIGestureRecognizer.swift b/Shared/Extensions/UIGestureRecognizer.swift index dbf46e426..b825df563 100644 --- a/Shared/Extensions/UIGestureRecognizer.swift +++ b/Shared/Extensions/UIGestureRecognizer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/UIHostingController.swift b/Shared/Extensions/UIHostingController.swift index da2258c7f..f633f8cc4 100644 --- a/Shared/Extensions/UIHostingController.swift +++ b/Shared/Extensions/UIHostingController.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/UIScreen.swift b/Shared/Extensions/UIScreen.swift index 70c5f8a79..6372d1c4e 100644 --- a/Shared/Extensions/UIScreen.swift +++ b/Shared/Extensions/UIScreen.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/URL.swift b/Shared/Extensions/URL.swift index e11a938e3..cffe97483 100644 --- a/Shared/Extensions/URL.swift +++ b/Shared/Extensions/URL.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/URLComponents.swift b/Shared/Extensions/URLComponents.swift index c2e04cacf..a7cf37ec4 100644 --- a/Shared/Extensions/URLComponents.swift +++ b/Shared/Extensions/URLComponents.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/URLResponse.swift b/Shared/Extensions/URLResponse.swift index ab497e15b..c95234aa7 100644 --- a/Shared/Extensions/URLResponse.swift +++ b/Shared/Extensions/URLResponse.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/URLSessionConfiguration.swift b/Shared/Extensions/URLSessionConfiguration.swift index e683d1bf7..4d2bbf2d7 100644 --- a/Shared/Extensions/URLSessionConfiguration.swift +++ b/Shared/Extensions/URLSessionConfiguration.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/VerticalAlignment.swift b/Shared/Extensions/VerticalAlignment.swift index 5b0764db1..06e635fd5 100644 --- a/Shared/Extensions/VerticalAlignment.swift +++ b/Shared/Extensions/VerticalAlignment.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift b/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift index 875c5f952..fedb76a25 100644 --- a/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift +++ b/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/ViewExtensions/Backport/Backport.swift b/Shared/Extensions/ViewExtensions/Backport/Backport.swift index 30844f154..639db398e 100644 --- a/Shared/Extensions/ViewExtensions/Backport/Backport.swift +++ b/Shared/Extensions/ViewExtensions/Backport/Backport.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift index 7bda41574..dfcfa7e2b 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift index 526377ea9..84a6daf09 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift index 757052d97..4cf312d4e 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift b/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift index e151c38ae..8807fa085 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift index 888dda70c..8bbc1f329 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift index c85402d0b..a158440c6 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift index 1fd538e70..945d1993f 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift index 5ccc3ff7b..c27074867 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift index ef07415e6..6e014e07d 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift index 5a524d70d..5a6225b4b 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift index b098a68f0..09b9a7d85 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift index 67a54155a..d66869d65 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/PreferenceKeys.swift b/Shared/Extensions/ViewExtensions/PreferenceKeys.swift index 5afbcd35d..7357640e4 100644 --- a/Shared/Extensions/ViewExtensions/PreferenceKeys.swift +++ b/Shared/Extensions/ViewExtensions/PreferenceKeys.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions/ViewExtensions.swift index 3341c9210..7a663a1b9 100644 --- a/Shared/Extensions/ViewExtensions/ViewExtensions.swift +++ b/Shared/Extensions/ViewExtensions/ViewExtensions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/AppAppearance.swift b/Shared/Objects/AppAppearance.swift index 256ce227f..80ce18bdb 100644 --- a/Shared/Objects/AppAppearance.swift +++ b/Shared/Objects/AppAppearance.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ArrayBuilder.swift b/Shared/Objects/ArrayBuilder.swift index 0a8e34ad1..6a5a1ae00 100644 --- a/Shared/Objects/ArrayBuilder.swift +++ b/Shared/Objects/ArrayBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/BindingBox.swift b/Shared/Objects/BindingBox.swift index 175eac266..155fd8c4e 100644 --- a/Shared/Objects/BindingBox.swift +++ b/Shared/Objects/BindingBox.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Objects/CaseIterablePicker.swift b/Shared/Objects/CaseIterablePicker.swift index 538cfd723..e4ce6e4a7 100644 --- a/Shared/Objects/CaseIterablePicker.swift +++ b/Shared/Objects/CaseIterablePicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/ChannelProgram.swift b/Shared/Objects/ChannelProgram.swift index 43dc567db..d5fd2e77b 100644 --- a/Shared/Objects/ChannelProgram.swift +++ b/Shared/Objects/ChannelProgram.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/CommaStringBuilder.swift b/Shared/Objects/CommaStringBuilder.swift index 7456844f8..bbaff28f8 100644 --- a/Shared/Objects/CommaStringBuilder.swift +++ b/Shared/Objects/CommaStringBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/CurrentDate.swift b/Shared/Objects/CurrentDate.swift index 4d4b840c5..2242e9959 100644 --- a/Shared/Objects/CurrentDate.swift +++ b/Shared/Objects/CurrentDate.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Objects/CustomDeviceProfileAction.swift b/Shared/Objects/CustomDeviceProfileAction.swift index f59fe94fa..b9f6dcfeb 100644 --- a/Shared/Objects/CustomDeviceProfileAction.swift +++ b/Shared/Objects/CustomDeviceProfileAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift b/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift index fc5444240..7e539798f 100644 --- a/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift +++ b/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift b/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift index 0c7cdbabf..fa0798e98 100644 --- a/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift +++ b/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Displayable.swift b/Shared/Objects/Displayable.swift index 935efd34f..c4278ba45 100644 --- a/Shared/Objects/Displayable.swift +++ b/Shared/Objects/Displayable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Eventful.swift b/Shared/Objects/Eventful.swift index 258d9f142..1bf7b7a15 100644 --- a/Shared/Objects/Eventful.swift +++ b/Shared/Objects/Eventful.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Objects/GestureAction.swift b/Shared/Objects/GestureAction.swift index de4e37b17..59403a628 100644 --- a/Shared/Objects/GestureAction.swift +++ b/Shared/Objects/GestureAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ImageSource.swift b/Shared/Objects/ImageSource.swift index b08c43c24..d131d81e7 100644 --- a/Shared/Objects/ImageSource.swift +++ b/Shared/Objects/ImageSource.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemArrayElements.swift b/Shared/Objects/ItemArrayElements.swift index 23b2df193..f9e00c8aa 100644 --- a/Shared/Objects/ItemArrayElements.swift +++ b/Shared/Objects/ItemArrayElements.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/AnyItemFilter.swift b/Shared/Objects/ItemFilter/AnyItemFilter.swift index 412ba1e4a..7b14470ce 100644 --- a/Shared/Objects/ItemFilter/AnyItemFilter.swift +++ b/Shared/Objects/ItemFilter/AnyItemFilter.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemFilter.swift b/Shared/Objects/ItemFilter/ItemFilter.swift index 1f31c1667..514b73aef 100644 --- a/Shared/Objects/ItemFilter/ItemFilter.swift +++ b/Shared/Objects/ItemFilter/ItemFilter.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ItemFilter/ItemFilterCollection.swift b/Shared/Objects/ItemFilter/ItemFilterCollection.swift index 3b3f4e9eb..c62c35964 100644 --- a/Shared/Objects/ItemFilter/ItemFilterCollection.swift +++ b/Shared/Objects/ItemFilter/ItemFilterCollection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ItemFilter/ItemFilterType.swift b/Shared/Objects/ItemFilter/ItemFilterType.swift index 9547c58b1..422fe3394 100644 --- a/Shared/Objects/ItemFilter/ItemFilterType.swift +++ b/Shared/Objects/ItemFilter/ItemFilterType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ItemFilter/ItemGenre.swift b/Shared/Objects/ItemFilter/ItemGenre.swift index 9433ab0ad..4c33b64cd 100644 --- a/Shared/Objects/ItemFilter/ItemGenre.swift +++ b/Shared/Objects/ItemFilter/ItemGenre.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemLetter.swift b/Shared/Objects/ItemFilter/ItemLetter.swift index 9b8156b3d..cba593c30 100644 --- a/Shared/Objects/ItemFilter/ItemLetter.swift +++ b/Shared/Objects/ItemFilter/ItemLetter.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemSortBy.swift b/Shared/Objects/ItemFilter/ItemSortBy.swift index c0eb0dbb4..f1ef59c40 100644 --- a/Shared/Objects/ItemFilter/ItemSortBy.swift +++ b/Shared/Objects/ItemFilter/ItemSortBy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemTag.swift b/Shared/Objects/ItemFilter/ItemTag.swift index fc2b34221..4805cd9b2 100644 --- a/Shared/Objects/ItemFilter/ItemTag.swift +++ b/Shared/Objects/ItemFilter/ItemTag.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemYear.swift b/Shared/Objects/ItemFilter/ItemYear.swift index 10abc7b98..30d1f2d05 100644 --- a/Shared/Objects/ItemFilter/ItemYear.swift +++ b/Shared/Objects/ItemFilter/ItemYear.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemViewType.swift b/Shared/Objects/ItemViewType.swift index 0fefc53dd..e4e32e1a6 100644 --- a/Shared/Objects/ItemViewType.swift +++ b/Shared/Objects/ItemViewType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/LibraryDisplayType.swift b/Shared/Objects/LibraryDisplayType.swift index 472b8fd14..a8340c72e 100644 --- a/Shared/Objects/LibraryDisplayType.swift +++ b/Shared/Objects/LibraryDisplayType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/LibraryParent/LibraryParent.swift b/Shared/Objects/LibraryParent/LibraryParent.swift index 2e56805a0..0165513ba 100644 --- a/Shared/Objects/LibraryParent/LibraryParent.swift +++ b/Shared/Objects/LibraryParent/LibraryParent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/LibraryParent/TitledLibraryParent.swift b/Shared/Objects/LibraryParent/TitledLibraryParent.swift index 53101f8f3..706195861 100644 --- a/Shared/Objects/LibraryParent/TitledLibraryParent.swift +++ b/Shared/Objects/LibraryParent/TitledLibraryParent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/MediaComponents/AudoCodec.swift b/Shared/Objects/MediaComponents/AudoCodec.swift index 5339aa715..55f76f488 100644 --- a/Shared/Objects/MediaComponents/AudoCodec.swift +++ b/Shared/Objects/MediaComponents/AudoCodec.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/MediaComponents/MediaContainer.swift b/Shared/Objects/MediaComponents/MediaContainer.swift index db61d8c21..678b15e61 100644 --- a/Shared/Objects/MediaComponents/MediaContainer.swift +++ b/Shared/Objects/MediaComponents/MediaContainer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/MediaComponents/SubtitleFormat.swift b/Shared/Objects/MediaComponents/SubtitleFormat.swift index 0f3d55c09..c456512d1 100644 --- a/Shared/Objects/MediaComponents/SubtitleFormat.swift +++ b/Shared/Objects/MediaComponents/SubtitleFormat.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/MediaComponents/VideoCodec.swift b/Shared/Objects/MediaComponents/VideoCodec.swift index 93b92d947..4f5ef75ba 100644 --- a/Shared/Objects/MediaComponents/VideoCodec.swift +++ b/Shared/Objects/MediaComponents/VideoCodec.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/NotificationSet.swift b/Shared/Objects/NotificationSet.swift index 5dc37da92..8aa1934d7 100644 --- a/Shared/Objects/NotificationSet.swift +++ b/Shared/Objects/NotificationSet.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/OverlayType.swift b/Shared/Objects/OverlayType.swift index b83787f26..53b726c1b 100644 --- a/Shared/Objects/OverlayType.swift +++ b/Shared/Objects/OverlayType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PanDirectionGestureRecognizer.swift b/Shared/Objects/PanDirectionGestureRecognizer.swift index 751695119..97fbcb9df 100644 --- a/Shared/Objects/PanDirectionGestureRecognizer.swift +++ b/Shared/Objects/PanDirectionGestureRecognizer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift b/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift index a40a84cee..0cbbdcfc2 100644 --- a/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift +++ b/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift b/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift index 800e4c4f2..fb8903100 100644 --- a/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift +++ b/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift index 70dd0e001..a655a6fcc 100644 --- a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift +++ b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift index 5cc26a02e..e39647b35 100644 --- a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift +++ b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackDeviceProfile.swift b/Shared/Objects/PlaybackDeviceProfile.swift index cc3336e52..a3b1c8d89 100644 --- a/Shared/Objects/PlaybackDeviceProfile.swift +++ b/Shared/Objects/PlaybackDeviceProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackSpeed.swift b/Shared/Objects/PlaybackSpeed.swift index 32e52c405..40768f2d0 100644 --- a/Shared/Objects/PlaybackSpeed.swift +++ b/Shared/Objects/PlaybackSpeed.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Poster.swift b/Shared/Objects/Poster.swift index cdf18e0b5..b512334fd 100644 --- a/Shared/Objects/Poster.swift +++ b/Shared/Objects/Poster.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/PosterDisplayType.swift b/Shared/Objects/PosterDisplayType.swift index 4eaff949b..18e885257 100644 --- a/Shared/Objects/PosterDisplayType.swift +++ b/Shared/Objects/PosterDisplayType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/RepeatingTimer.swift b/Shared/Objects/RepeatingTimer.swift index 216ee63d7..c3cc8e12e 100644 --- a/Shared/Objects/RepeatingTimer.swift +++ b/Shared/Objects/RepeatingTimer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/RoundedCorner.swift b/Shared/Objects/RoundedCorner.swift index 2845e11a6..ccd5e0ba1 100644 --- a/Shared/Objects/RoundedCorner.swift +++ b/Shared/Objects/RoundedCorner.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/ScalingButtonStyle.swift b/Shared/Objects/ScalingButtonStyle.swift index ed89f273f..1cde10705 100644 --- a/Shared/Objects/ScalingButtonStyle.swift +++ b/Shared/Objects/ScalingButtonStyle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/SelectUserServerSelection.swift b/Shared/Objects/SelectUserServerSelection.swift index a1b83b050..ded2602b9 100644 --- a/Shared/Objects/SelectUserServerSelection.swift +++ b/Shared/Objects/SelectUserServerSelection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/SeriesStatus.swift b/Shared/Objects/SeriesStatus.swift index a4548bbc5..d82aae1c0 100644 --- a/Shared/Objects/SeriesStatus.swift +++ b/Shared/Objects/SeriesStatus.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/SliderType.swift b/Shared/Objects/SliderType.swift index e041b5203..fcf5bab04 100644 --- a/Shared/Objects/SliderType.swift +++ b/Shared/Objects/SliderType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/Stateful.swift b/Shared/Objects/Stateful.swift index 4647ed8a5..e2c602ac7 100644 --- a/Shared/Objects/Stateful.swift +++ b/Shared/Objects/Stateful.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Storable.swift b/Shared/Objects/Storable.swift index 887bba6d4..6588e0ed6 100644 --- a/Shared/Objects/Storable.swift +++ b/Shared/Objects/Storable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/StreamType.swift b/Shared/Objects/StreamType.swift index e715db8a1..7d504fb72 100644 --- a/Shared/Objects/StreamType.swift +++ b/Shared/Objects/StreamType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/SupportedCaseIterable.swift b/Shared/Objects/SupportedCaseIterable.swift index 46fc0684e..4c5b5069e 100644 --- a/Shared/Objects/SupportedCaseIterable.swift +++ b/Shared/Objects/SupportedCaseIterable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/SystemImageable.swift b/Shared/Objects/SystemImageable.swift index 9dbef46f8..87c4242de 100644 --- a/Shared/Objects/SystemImageable.swift +++ b/Shared/Objects/SystemImageable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/TextPair.swift b/Shared/Objects/TextPair.swift index 8c9d95713..fb09f75ca 100644 --- a/Shared/Objects/TextPair.swift +++ b/Shared/Objects/TextPair.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/TimeStampType.swift b/Shared/Objects/TimeStampType.swift index 2ed77a7bb..51847bac7 100644 --- a/Shared/Objects/TimeStampType.swift +++ b/Shared/Objects/TimeStampType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/TimerProxy.swift b/Shared/Objects/TimerProxy.swift index 803761307..12febe876 100644 --- a/Shared/Objects/TimerProxy.swift +++ b/Shared/Objects/TimerProxy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/TrailingTimestampType.swift b/Shared/Objects/TrailingTimestampType.swift index 8bef539a4..916fb4948 100644 --- a/Shared/Objects/TrailingTimestampType.swift +++ b/Shared/Objects/TrailingTimestampType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/Trie.swift b/Shared/Objects/Trie.swift index eb194709b..35ece1e58 100644 --- a/Shared/Objects/Trie.swift +++ b/Shared/Objects/Trie.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // class Trie where Key.Element: Hashable { diff --git a/Shared/Objects/UserAccessPolicy.swift b/Shared/Objects/UserAccessPolicy.swift index d9783d070..d9fa13713 100644 --- a/Shared/Objects/UserAccessPolicy.swift +++ b/Shared/Objects/UserAccessPolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/UserPermissions.swift b/Shared/Objects/UserPermissions.swift index 706f5c3e5..e86b9a81b 100644 --- a/Shared/Objects/UserPermissions.swift +++ b/Shared/Objects/UserPermissions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Objects/UserSignInState.swift b/Shared/Objects/UserSignInState.swift index 8c2cdee4e..c0e98f3a7 100644 --- a/Shared/Objects/UserSignInState.swift +++ b/Shared/Objects/UserSignInState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/Utilities.swift b/Shared/Objects/Utilities.swift index 108ffdc87..5f3eaa541 100644 --- a/Shared/Objects/Utilities.swift +++ b/Shared/Objects/Utilities.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerActionButton.swift b/Shared/Objects/VideoPlayerActionButton.swift index fa9a5bb9e..e38f2770c 100644 --- a/Shared/Objects/VideoPlayerActionButton.swift +++ b/Shared/Objects/VideoPlayerActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/VideoPlayerJumpLength.swift b/Shared/Objects/VideoPlayerJumpLength.swift index fffb6b7d8..25597486e 100644 --- a/Shared/Objects/VideoPlayerJumpLength.swift +++ b/Shared/Objects/VideoPlayerJumpLength.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift index 2e4c1f57e..d0f7e0b6c 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift index 9e42a4e89..d63935291 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift index 1c2c12118..28ebeaee4 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType.swift index 38be61c58..f3d23c949 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/ServerDiscovery/ServerDiscovery.swift b/Shared/ServerDiscovery/ServerDiscovery.swift index 21b745182..61e229d74 100644 --- a/Shared/ServerDiscovery/ServerDiscovery.swift +++ b/Shared/ServerDiscovery/ServerDiscovery.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ServerDiscovery/ServerResponse.swift b/Shared/ServerDiscovery/ServerResponse.swift index dae85d03c..ed0ccc4bc 100644 --- a/Shared/ServerDiscovery/ServerResponse.swift +++ b/Shared/ServerDiscovery/ServerResponse.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Services/DownloadManager.swift b/Shared/Services/DownloadManager.swift index 2bc7f36a7..055499c0e 100644 --- a/Shared/Services/DownloadManager.swift +++ b/Shared/Services/DownloadManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Services/DownloadTask.swift b/Shared/Services/DownloadTask.swift index 47fae8c46..59a639e5a 100644 --- a/Shared/Services/DownloadTask.swift +++ b/Shared/Services/DownloadTask.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Services/Keychain.swift b/Shared/Services/Keychain.swift index be4f83e10..d945fff4c 100644 --- a/Shared/Services/Keychain.swift +++ b/Shared/Services/Keychain.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Services/LogManager.swift b/Shared/Services/LogManager.swift index f7b75567d..27c915477 100644 --- a/Shared/Services/LogManager.swift +++ b/Shared/Services/LogManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/Services/Notifications.swift b/Shared/Services/Notifications.swift index 35794ff95..cd0cbd51a 100644 --- a/Shared/Services/Notifications.swift +++ b/Shared/Services/Notifications.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Services/SwiftfinDefaults.swift b/Shared/Services/SwiftfinDefaults.swift index 96a1153d4..a0e3822b3 100644 --- a/Shared/Services/SwiftfinDefaults.swift +++ b/Shared/Services/SwiftfinDefaults.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Services/UserSession.swift b/Shared/Services/UserSession.swift index 952f7aee9..942341e94 100644 --- a/Shared/Services/UserSession.swift +++ b/Shared/Services/UserSession.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreData diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 55d0a0247..e09971d38 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -16,8 +16,6 @@ internal enum L10n { internal static let absolute = L10n.tr("Localizable", "absolute", fallback: "Absolute") /// Accent Color internal static let accentColor = L10n.tr("Localizable", "accentColor", fallback: "Accent Color") - /// Some views may need an app restart to update. - internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.") /// Access internal static let access = L10n.tr("Localizable", "access", fallback: "Access") /// Accessibility @@ -106,8 +104,6 @@ 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 @@ -1312,8 +1308,6 @@ 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/SwiftfinStore/StoredValue/StoredValue.swift b/Shared/SwiftfinStore/StoredValue/StoredValue.swift index 5438c7eec..3085c1b21 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValue.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValue.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift b/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift index edf39eb37..f5d6e09bb 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift b/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift index ef20d0c23..29c5ed265 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift b/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift index 476b9a4e8..7e12270d3 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift b/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift index 14bc9c14f..8f9b01ba5 100644 --- a/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift +++ b/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift b/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift index 4467f35a4..bd2206f84 100644 --- a/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift +++ b/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index 593b67ded..17e67b670 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/SwiftinStore+UserState.swift b/Shared/SwiftfinStore/SwiftinStore+UserState.swift index fd328264e..de29c34f7 100644 --- a/Shared/SwiftfinStore/SwiftinStore+UserState.swift +++ b/Shared/SwiftfinStore/SwiftinStore+UserState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift b/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift index 316a47768..3a878ca48 100644 --- a/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift +++ b/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift b/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift index de961b8c4..d89c90d33 100644 --- a/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift +++ b/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V1Schema/V1UserModel.swift b/Shared/SwiftfinStore/V1Schema/V1UserModel.swift index 2618a689e..787497144 100644 --- a/Shared/SwiftfinStore/V1Schema/V1UserModel.swift +++ b/Shared/SwiftfinStore/V1Schema/V1UserModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift b/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift index 7d269ac98..d68438324 100644 --- a/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift +++ b/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V2Schema/V2AnyData.swift b/Shared/SwiftfinStore/V2Schema/V2AnyData.swift index c31142f08..9145639bb 100644 --- a/Shared/SwiftfinStore/V2Schema/V2AnyData.swift +++ b/Shared/SwiftfinStore/V2Schema/V2AnyData.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift b/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift index c416f12d1..e57ba0d63 100644 --- a/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift +++ b/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V2Schema/V2UserModel.swift b/Shared/SwiftfinStore/V2Schema/V2UserModel.swift index 931f143ab..698db5d2a 100644 --- a/Shared/SwiftfinStore/V2Schema/V2UserModel.swift +++ b/Shared/SwiftfinStore/V2Schema/V2UserModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift b/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift index f191e291b..ee4f304ee 100644 --- a/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift b/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift index d2dae1244..00c930408 100644 --- a/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift b/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift index 2ad50f1b5..79ce618e3 100644 --- a/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift b/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift index f1b41a337..ac4f35a0a 100644 --- a/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift b/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift index 91878612f..3917b997f 100644 --- a/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift b/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift index 4a0e1a0e2..3205ca85c 100644 --- a/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift +++ b/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift index e371a3fa4..5f2fb8f41 100644 --- a/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift index 420db69e8..3d90edc28 100644 --- a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift index 05b9c0566..bc193b5f3 100644 --- a/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ChannelLibraryViewModel.swift b/Shared/ViewModels/ChannelLibraryViewModel.swift index 754d4e4b7..98cb6ad32 100644 --- a/Shared/ViewModels/ChannelLibraryViewModel.swift +++ b/Shared/ViewModels/ChannelLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 480e095a6..8b2f1db09 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/DownloadListViewModel.swift b/Shared/ViewModels/DownloadListViewModel.swift index f68a85e1c..c9fb3a709 100644 --- a/Shared/ViewModels/DownloadListViewModel.swift +++ b/Shared/ViewModels/DownloadListViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/ViewModels/FilterViewModel.swift b/Shared/ViewModels/FilterViewModel.swift index 3f2f40ba4..87799f1bc 100644 --- a/Shared/ViewModels/FilterViewModel.swift +++ b/Shared/ViewModels/FilterViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 8454357e6..008c0b75f 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift b/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift index a8138b038..1ffa5d44a 100644 --- a/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift b/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift index 55e3419bb..9be23a4b4 100644 --- a/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift index 6edac7055..c4848ab36 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift index baa76dfa9..aea2dd6ea 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift index 3386c7db6..fd8a1eadf 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift index 2c91995c2..129696a53 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift index 1e67c904b..8403963da 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index b2210f5a7..e9f43f62d 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift b/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift index bbd20192c..04844c1a1 100644 --- a/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift index 4d02d244b..be519c389 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift index 368af421c..d82c539b0 100644 --- a/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift index 39454f150..c3384427d 100644 --- a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index 80dc78553..a62ae0991 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift b/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift index 4c8f340af..c2bf4c3e5 100644 --- a/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift index 0f9cb0bed..527992ccb 100644 --- a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift index bfed1f2cf..f275d6405 100644 --- a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift index f4c422099..24fc3f31c 100644 --- a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift index 0bf9b6c50..aa5fd8f0e 100644 --- a/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift index dd7231950..d873709d5 100644 --- a/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift index 4edcb87f3..e3320520e 100644 --- a/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift index 1c6cdfc45..b780f662b 100644 --- a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift b/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift index 59347b782..6cf3d1573 100644 --- a/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LiveVideoPlayerManager.swift b/Shared/ViewModels/LiveVideoPlayerManager.swift index 1f2307706..5a0fce2d5 100644 --- a/Shared/ViewModels/LiveVideoPlayerManager.swift +++ b/Shared/ViewModels/LiveVideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/MediaViewModel/MediaType.swift b/Shared/ViewModels/MediaViewModel/MediaType.swift index b25bd02fe..98ffb78e3 100644 --- a/Shared/ViewModels/MediaViewModel/MediaType.swift +++ b/Shared/ViewModels/MediaViewModel/MediaType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/MediaViewModel/MediaViewModel.swift b/Shared/ViewModels/MediaViewModel/MediaViewModel.swift index cf2046941..903d0469c 100644 --- a/Shared/ViewModels/MediaViewModel/MediaViewModel.swift +++ b/Shared/ViewModels/MediaViewModel/MediaViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/ViewModels/ParentalRatingsViewModel.swift b/Shared/ViewModels/ParentalRatingsViewModel.swift index 4dd943c79..0c4e9b015 100644 --- a/Shared/ViewModels/ParentalRatingsViewModel.swift +++ b/Shared/ViewModels/ParentalRatingsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ProgramsViewModel.swift b/Shared/ViewModels/ProgramsViewModel.swift index 67c90d95c..64e661e0b 100644 --- a/Shared/ViewModels/ProgramsViewModel.swift +++ b/Shared/ViewModels/ProgramsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift b/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift index 293258fbe..9228842fb 100644 --- a/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift +++ b/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ResetUserPasswordViewModel.swift b/Shared/ViewModels/ResetUserPasswordViewModel.swift index dd028605d..ce15b455f 100644 --- a/Shared/ViewModels/ResetUserPasswordViewModel.swift +++ b/Shared/ViewModels/ResetUserPasswordViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/SearchViewModel.swift b/Shared/ViewModels/SearchViewModel.swift index 3de164f9b..88f474e21 100644 --- a/Shared/ViewModels/SearchViewModel.swift +++ b/Shared/ViewModels/SearchViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/SelectUserViewModel.swift b/Shared/ViewModels/SelectUserViewModel.swift index 9997ba4ff..6fc803326 100644 --- a/Shared/ViewModels/SelectUserViewModel.swift +++ b/Shared/ViewModels/SelectUserViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ServerCheckViewModel.swift b/Shared/ViewModels/ServerCheckViewModel.swift index a25963900..a4d7b875f 100644 --- a/Shared/ViewModels/ServerCheckViewModel.swift +++ b/Shared/ViewModels/ServerCheckViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ServerConnectionViewModel.swift b/Shared/ViewModels/ServerConnectionViewModel.swift index 778e7e2f2..53977928b 100644 --- a/Shared/ViewModels/ServerConnectionViewModel.swift +++ b/Shared/ViewModels/ServerConnectionViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/ViewModels/ServerLogsViewModel.swift b/Shared/ViewModels/ServerLogsViewModel.swift index 86ef22f65..e63f837fc 100644 --- a/Shared/ViewModels/ServerLogsViewModel.swift +++ b/Shared/ViewModels/ServerLogsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 41abc488d..820f07516 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/ViewModels/UserLocalSecurityViewModel.swift b/Shared/ViewModels/UserLocalSecurityViewModel.swift index 5de3f2387..422027579 100644 --- a/Shared/ViewModels/UserLocalSecurityViewModel.swift +++ b/Shared/ViewModels/UserLocalSecurityViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/UserProfileImageViewModel.swift b/Shared/ViewModels/UserProfileImageViewModel.swift index 16fdeafbd..2fbfe7626 100644 --- a/Shared/ViewModels/UserProfileImageViewModel.swift +++ b/Shared/ViewModels/UserProfileImageViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 63138fba5..61903be2b 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift b/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift index 82eab8b8f..d1fdd7588 100644 --- a/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift +++ b/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift b/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift index a50eb0d99..89c3f22f5 100644 --- a/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift +++ b/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift b/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift index 006bc0e9d..9f0482256 100644 --- a/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift +++ b/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVFoundation diff --git a/Shared/ViewModels/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel.swift index e3636104f..af9957b91 100644 --- a/Shared/ViewModels/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index c00c3e451..fc614814c 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift index c459d3d58..00c64838e 100644 --- a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift +++ b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // TODO: IMPLEMENT BUTTON OVERRIDES IN `PreferencesView` PACKAGE diff --git a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift index f4231d8b2..feafce530 100644 --- a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift +++ b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // TODO: IMPLEMENT BUTTON OVERRIDES IN `PreferencesView` PACKAGE diff --git a/Swiftfin tvOS/App/SwiftfinApp.swift b/Swiftfin tvOS/App/SwiftfinApp.swift index 7683d5521..361cd2f07 100644 --- a/Swiftfin tvOS/App/SwiftfinApp.swift +++ b/Swiftfin tvOS/App/SwiftfinApp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Swiftfin tvOS/Components/CinematicBackgroundView.swift b/Swiftfin tvOS/Components/CinematicBackgroundView.swift index 23606d345..a224c9758 100644 --- a/Swiftfin tvOS/Components/CinematicBackgroundView.swift +++ b/Swiftfin tvOS/Components/CinematicBackgroundView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/Components/CinematicItemSelector.swift b/Swiftfin tvOS/Components/CinematicItemSelector.swift index 83ad76709..31c1bb222 100644 --- a/Swiftfin tvOS/Components/CinematicItemSelector.swift +++ b/Swiftfin tvOS/Components/CinematicItemSelector.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/Components/DotHStack.swift b/Swiftfin tvOS/Components/DotHStack.swift index 191955931..6cb39feb3 100644 --- a/Swiftfin tvOS/Components/DotHStack.swift +++ b/Swiftfin tvOS/Components/DotHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/EnumPickerView.swift b/Swiftfin tvOS/Components/EnumPickerView.swift index 5ea8fcea3..87640f8ab 100644 --- a/Swiftfin tvOS/Components/EnumPickerView.swift +++ b/Swiftfin tvOS/Components/EnumPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/InlineEnumToggle.swift b/Swiftfin tvOS/Components/InlineEnumToggle.swift index 18f24c7a7..c38fbd473 100644 --- a/Swiftfin tvOS/Components/InlineEnumToggle.swift +++ b/Swiftfin tvOS/Components/InlineEnumToggle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift b/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift index be5e9e0c2..c66ca9504 100644 --- a/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift +++ b/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/ListRowButton.swift b/Swiftfin tvOS/Components/ListRowButton.swift index 0c96b5b4a..1774fb62e 100644 --- a/Swiftfin tvOS/Components/ListRowButton.swift +++ b/Swiftfin tvOS/Components/ListRowButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/NonePosterButton.swift b/Swiftfin tvOS/Components/NonePosterButton.swift index 33629f022..e533275ad 100644 --- a/Swiftfin tvOS/Components/NonePosterButton.swift +++ b/Swiftfin tvOS/Components/NonePosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift b/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift index d320123cb..9b958c649 100644 --- a/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift +++ b/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index 1b977d7db..6ad58f255 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Components/PosterHStack.swift b/Swiftfin tvOS/Components/PosterHStack.swift index 10af0e780..244b1c48a 100644 --- a/Swiftfin tvOS/Components/PosterHStack.swift +++ b/Swiftfin tvOS/Components/PosterHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin tvOS/Components/SFSymbolButton.swift b/Swiftfin tvOS/Components/SFSymbolButton.swift index 40b47a4ac..523307a29 100644 --- a/Swiftfin tvOS/Components/SFSymbolButton.swift +++ b/Swiftfin tvOS/Components/SFSymbolButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/SeeAllPosterButton.swift b/Swiftfin tvOS/Components/SeeAllPosterButton.swift index 947de6e82..11617446e 100644 --- a/Swiftfin tvOS/Components/SeeAllPosterButton.swift +++ b/Swiftfin tvOS/Components/SeeAllPosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/ServerButton.swift b/Swiftfin tvOS/Components/ServerButton.swift index ef7f7653f..4565e0227 100644 --- a/Swiftfin tvOS/Components/ServerButton.swift +++ b/Swiftfin tvOS/Components/ServerButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/SplitFormWindowView.swift b/Swiftfin tvOS/Components/SplitFormWindowView.swift index 20fb319c9..3d44c38cc 100644 --- a/Swiftfin tvOS/Components/SplitFormWindowView.swift +++ b/Swiftfin tvOS/Components/SplitFormWindowView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/SplitLoginWindowView.swift b/Swiftfin tvOS/Components/SplitLoginWindowView.swift index f724a38e3..5cc6ce502 100644 --- a/Swiftfin tvOS/Components/SplitLoginWindowView.swift +++ b/Swiftfin tvOS/Components/SplitLoginWindowView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin tvOS/Components/StepperView.swift b/Swiftfin tvOS/Components/StepperView.swift index 945b38921..3c7492d26 100644 --- a/Swiftfin tvOS/Components/StepperView.swift +++ b/Swiftfin tvOS/Components/StepperView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift b/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift index 1014c3931..75318c5f4 100644 --- a/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift +++ b/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Extensions/View/View-tvOS.swift b/Swiftfin tvOS/Extensions/View/View-tvOS.swift index 6630ee3a3..233da0c3b 100644 --- a/Swiftfin tvOS/Extensions/View/View-tvOS.swift +++ b/Swiftfin tvOS/Extensions/View/View-tvOS.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/ImageButtonStyle.swift b/Swiftfin tvOS/ImageButtonStyle.swift index 1b9d16eba..202564441 100644 --- a/Swiftfin tvOS/ImageButtonStyle.swift +++ b/Swiftfin tvOS/ImageButtonStyle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // struct ImageButtonStyle: ButtonStyle { diff --git a/Swiftfin tvOS/Objects/FocusGuide.swift b/Swiftfin tvOS/Objects/FocusGuide.swift index 027554911..00c9014ba 100644 --- a/Swiftfin tvOS/Objects/FocusGuide.swift +++ b/Swiftfin tvOS/Objects/FocusGuide.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/AppLoadingView.swift b/Swiftfin tvOS/Views/AppLoadingView.swift index 008752333..1fcdc22ce 100644 --- a/Swiftfin tvOS/Views/AppLoadingView.swift +++ b/Swiftfin tvOS/Views/AppLoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/BasicAppSettingsView.swift b/Swiftfin tvOS/Views/BasicAppSettingsView.swift index e007f2b9d..3cd265ab5 100644 --- a/Swiftfin tvOS/Views/BasicAppSettingsView.swift +++ b/Swiftfin tvOS/Views/BasicAppSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift index f8cae264c..8a2ad4c48 100644 --- a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift +++ b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift b/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift index eb0741e33..997b58a3e 100644 --- a/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift +++ b/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift b/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift index 92fca3f69..11c08d6ae 100644 --- a/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift +++ b/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift b/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift index 2fd746305..da2f10e81 100644 --- a/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift +++ b/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/Views/FontPickerView.swift b/Swiftfin tvOS/Views/FontPickerView.swift index 8b76365cc..c82ed374f 100644 --- a/Swiftfin tvOS/Views/FontPickerView.swift +++ b/Swiftfin tvOS/Views/FontPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift b/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift index 5bd3a6f56..7b5036343 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift b/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift index 5609c8b1a..a8fc2b2c5 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift index 84b2cd167..a4b36ce3c 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift index 5e14053b8..b8a8209da 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift index 3d627b332..cdd2d1502 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift b/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift index 428f3cdc1..b7d62961a 100644 --- a/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift +++ b/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/HomeView/HomeView.swift b/Swiftfin tvOS/Views/HomeView/HomeView.swift index 16e4919c3..5cfb0b65b 100644 --- a/Swiftfin tvOS/Views/HomeView/HomeView.swift +++ b/Swiftfin tvOS/Views/HomeView/HomeView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemOverviewView.swift b/Swiftfin tvOS/Views/ItemOverviewView.swift index 7b918f2c2..8cd80faf2 100644 --- a/Swiftfin tvOS/Views/ItemOverviewView.swift +++ b/Swiftfin tvOS/Views/ItemOverviewView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift index 37f06a662..04cf0ec8f 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift index f2074d012..1101fd285 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift index 439a15d4d..cfec802e3 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift index ab1489204..8d01a060f 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift index 192279eb6..1bc83e6af 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift index 5209c5bed..2652597ac 100644 --- a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift index 15fcaf241..cfe932b8a 100644 --- a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift index be9192504..b84a0fe26 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift index 8c1cb7e2f..ed2b25612 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift index 24434ce5e..326dd52c1 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift index 36433dee9..f5d259256 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift index 86ae5460d..e7ea09a3c 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift index dafee4615..0660a6f34 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift index 4a31af6a9..26a35e408 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift index ff66b787b..1d939f635 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift index 6f82251ca..a8328b88d 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift index 891707aa6..d553901bd 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift index c957ca372..e976b233b 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift index 80c08508c..7ad6e88c5 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift index ec4ee0d77..4352bcc87 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index 2cf9f967c..d5302d846 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift index 4b3083866..30b57b911 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift index a9be264d6..6a9072697 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift index f0071d025..5edee9ef6 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift b/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift index e727bfc77..c71d05384 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift index 992985e5c..f6e49a756 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift index 868cf397d..595a7b0c2 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift index f9adec4c0..4bfdf9331 100644 --- a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift index ace4a254c..e541bc39e 100644 --- a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index 2a736e5bd..cce801081 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift index ca265576b..772216600 100644 --- a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift index 98bfa64e6..820a4ba6f 100644 --- a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift index d1ea1e9a4..33384541f 100644 --- a/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift +++ b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift index 8a31864df..bcd00e1c5 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift index c1eee1093..745c4c396 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/LearnMoreModal.swift b/Swiftfin tvOS/Views/LearnMoreModal.swift index ae551b31a..6639e4c08 100644 --- a/Swiftfin tvOS/Views/LearnMoreModal.swift +++ b/Swiftfin tvOS/Views/LearnMoreModal.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/MediaSourceInfoView.swift b/Swiftfin tvOS/Views/MediaSourceInfoView.swift index d449c996a..00cc32b10 100644 --- a/Swiftfin tvOS/Views/MediaSourceInfoView.swift +++ b/Swiftfin tvOS/Views/MediaSourceInfoView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift b/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift index a5534e5d9..a8457adf1 100644 --- a/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift +++ b/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/MediaView/MediaView.swift b/Swiftfin tvOS/Views/MediaView/MediaView.swift index 6142869a8..961f95b3a 100644 --- a/Swiftfin tvOS/Views/MediaView/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView/MediaView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift b/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift index 34939e5a4..f7236fd43 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift b/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift index 75348c656..8c8882d0b 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift index d1a8d1531..c91bfeda1 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift index f0d9bf967..5d65d1029 100644 --- a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift +++ b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift index 793b809a5..7c5ad24af 100644 --- a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift +++ b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift b/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift index fc621108d..686854cf0 100644 --- a/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift +++ b/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/QuickConnectView.swift b/Swiftfin tvOS/Views/QuickConnectView.swift index c2d609079..ad5e68939 100644 --- a/Swiftfin tvOS/Views/QuickConnectView.swift +++ b/Swiftfin tvOS/Views/QuickConnectView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/SearchView.swift b/Swiftfin tvOS/Views/SearchView.swift index 615532918..ce6b6ec97 100644 --- a/Swiftfin tvOS/Views/SearchView.swift +++ b/Swiftfin tvOS/Views/SearchView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift b/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift index 1ee50028c..7b4165da4 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift b/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift index 9761c4449..8147d44d5 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift b/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift index e9d4b8960..d0fe4a7a8 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift b/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift index f92e1a715..92c8ca2b9 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift index 5aa17a6c0..92b0ceea3 100644 --- a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin tvOS/Views/ServerDetailView.swift b/Swiftfin tvOS/Views/ServerDetailView.swift index 52e2d1f9e..c57205f6f 100644 --- a/Swiftfin tvOS/Views/ServerDetailView.swift +++ b/Swiftfin tvOS/Views/ServerDetailView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift index ccb04af03..53bdc87c6 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift index 8abfb342b..96d67b7d5 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift index df74872b7..9af28401c 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift index 07c20aa6a..0b0277bf8 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index f3db9a586..c29a80679 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift index dadfba623..5ea483ddf 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift index ec4f48117..2d0573da9 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift index 6b6e51077..6af5a3e6e 100644 --- a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift index 742c28f8a..565301c88 100644 --- a/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift b/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift index 9bb4369e0..fec284862 100644 --- a/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index 7ec282758..eacbd9a2b 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift index 264409779..3e84a39a4 100644 --- a/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift index 7e06660c1..b592881ed 100644 --- a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift +++ b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift index 2821c3f96..3a51e1ab0 100644 --- a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift index 90cbfb6be..74391c0a9 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift index 1f56fa192..b3da98b07 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift index 90554ba65..ccb836d76 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift index 014d5cf9e..20fe278a3 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift index 86843a5a0..a1e919037 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift index 35cf99343..79eca923a 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift index b56257247..f3ef9b2db 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift index be8947b85..f54e94fa6 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift index 76a036fad..e77fd32ab 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift index 3d81ae007..b235e2a92 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift index 83d56c57f..b182b25b6 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift index cdbfe9e16..4c7099b3b 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift index ce094353e..92594014c 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift index 9ddd284b8..b8f05750f 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift index 250291993..c43576b61 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift index 808a039ef..00885428b 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift index 3844d8bd4..5e82a2364 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift index a2e7e6c95..fb9551698 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift index 9929c015f..e2faadaac 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // Modification of https://github.com/zattoo/TvOSSlider diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift index fb52e37af..b5851700a 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift index 007277e20..4f867d41c 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift index 2d692b2b2..10360ec4d 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import PreferencesView diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift index beabc733e..b5a611ab0 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift index 7574f19b4..0c41f574a 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/App/AppDelegate.swift b/Swiftfin/App/AppDelegate.swift index 285954fc4..f3fd39149 100644 --- a/Swiftfin/App/AppDelegate.swift +++ b/Swiftfin/App/AppDelegate.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVFAudio diff --git a/Swiftfin/App/SwiftfinApp+ValueObservation.swift b/Swiftfin/App/SwiftfinApp+ValueObservation.swift index 03eeaf941..fc8098d79 100644 --- a/Swiftfin/App/SwiftfinApp+ValueObservation.swift +++ b/Swiftfin/App/SwiftfinApp+ValueObservation.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/App/SwiftfinApp.swift b/Swiftfin/App/SwiftfinApp.swift index 1b61eade1..3636706a8 100644 --- a/Swiftfin/App/SwiftfinApp.swift +++ b/Swiftfin/App/SwiftfinApp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Swiftfin/Components/BasicStepper.swift b/Swiftfin/Components/BasicStepper.swift index bf7e6daef..5c48123fa 100644 --- a/Swiftfin/Components/BasicStepper.swift +++ b/Swiftfin/Components/BasicStepper.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/CircularProgressView.swift b/Swiftfin/Components/CircularProgressView.swift index 6099f821a..1ec7a6de2 100644 --- a/Swiftfin/Components/CircularProgressView.swift +++ b/Swiftfin/Components/CircularProgressView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/CountryPicker.swift b/Swiftfin/Components/CountryPicker.swift index 2c903a559..f8049df4d 100644 --- a/Swiftfin/Components/CountryPicker.swift +++ b/Swiftfin/Components/CountryPicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/DelayedProgressView.swift b/Swiftfin/Components/DelayedProgressView.swift index 1314ec659..a55e738f4 100644 --- a/Swiftfin/Components/DelayedProgressView.swift +++ b/Swiftfin/Components/DelayedProgressView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Components/DotHStack.swift b/Swiftfin/Components/DotHStack.swift index 6c496c19a..0a3f68222 100644 --- a/Swiftfin/Components/DotHStack.swift +++ b/Swiftfin/Components/DotHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/ErrorView.swift b/Swiftfin/Components/ErrorView.swift index e5f55bd3f..ba68f514d 100644 --- a/Swiftfin/Components/ErrorView.swift +++ b/Swiftfin/Components/ErrorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/GestureView.swift b/Swiftfin/Components/GestureView.swift index 409b193ed..9a48541ea 100644 --- a/Swiftfin/Components/GestureView.swift +++ b/Swiftfin/Components/GestureView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Components/HourMinutePicker.swift b/Swiftfin/Components/HourMinutePicker.swift index b5573d349..1ad1b7b67 100644 --- a/Swiftfin/Components/HourMinutePicker.swift +++ b/Swiftfin/Components/HourMinutePicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/LandscapePosterProgressBar.swift b/Swiftfin/Components/LandscapePosterProgressBar.swift index 01f4ec918..e5bd4f12e 100644 --- a/Swiftfin/Components/LandscapePosterProgressBar.swift +++ b/Swiftfin/Components/LandscapePosterProgressBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/LanguagePicker.swift b/Swiftfin/Components/LanguagePicker.swift index 796855dca..e48816211 100644 --- a/Swiftfin/Components/LanguagePicker.swift +++ b/Swiftfin/Components/LanguagePicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/LearnMoreButton.swift b/Swiftfin/Components/LearnMoreButton.swift index c969b3d88..d4b9e4fea 100644 --- a/Swiftfin/Components/LearnMoreButton.swift +++ b/Swiftfin/Components/LearnMoreButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift index c0f33ac65..0960f7bea 100644 --- a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift +++ b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift b/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift index e4d37f902..f9275cb13 100644 --- a/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift +++ b/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/ListRow.swift b/Swiftfin/Components/ListRow.swift index 625485f69..260b81800 100644 --- a/Swiftfin/Components/ListRow.swift +++ b/Swiftfin/Components/ListRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/ListRowButton.swift b/Swiftfin/Components/ListRowButton.swift index ebd108133..5763d25d0 100644 --- a/Swiftfin/Components/ListRowButton.swift +++ b/Swiftfin/Components/ListRowButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/ListTitleSection.swift b/Swiftfin/Components/ListTitleSection.swift index 752f0d81f..b330dfff5 100644 --- a/Swiftfin/Components/ListTitleSection.swift +++ b/Swiftfin/Components/ListTitleSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift b/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift index 71e5167ad..75244ac28 100644 --- a/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift +++ b/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift b/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift index ba3462ca2..ad624b573 100644 --- a/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift +++ b/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/OrderedSectionSelectorView.swift b/Swiftfin/Components/OrderedSectionSelectorView.swift index 36b58a5dc..6f0b45777 100644 --- a/Swiftfin/Components/OrderedSectionSelectorView.swift +++ b/Swiftfin/Components/OrderedSectionSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/PillHStack.swift b/Swiftfin/Components/PillHStack.swift index 88cae310e..ce8707a7a 100644 --- a/Swiftfin/Components/PillHStack.swift +++ b/Swiftfin/Components/PillHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/PosterButton.swift b/Swiftfin/Components/PosterButton.swift index 4d0842c97..9010f6490 100644 --- a/Swiftfin/Components/PosterButton.swift +++ b/Swiftfin/Components/PosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/PosterHStack.swift b/Swiftfin/Components/PosterHStack.swift index b8cd9d9d7..df149dacc 100644 --- a/Swiftfin/Components/PosterHStack.swift +++ b/Swiftfin/Components/PosterHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Components/PrimaryButton.swift b/Swiftfin/Components/PrimaryButton.swift index b65fe36a8..87cee22aa 100644 --- a/Swiftfin/Components/PrimaryButton.swift +++ b/Swiftfin/Components/PrimaryButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/SeeAllButton.swift b/Swiftfin/Components/SeeAllButton.swift index 8a47e193e..d50f94b46 100644 --- a/Swiftfin/Components/SeeAllButton.swift +++ b/Swiftfin/Components/SeeAllButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/SettingsBarButton.swift b/Swiftfin/Components/SettingsBarButton.swift index 5aba32614..1d1828d93 100644 --- a/Swiftfin/Components/SettingsBarButton.swift +++ b/Swiftfin/Components/SettingsBarButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin/Components/Slider/CapsuleSlider.swift b/Swiftfin/Components/Slider/CapsuleSlider.swift index 0d64eb449..4bfd80623 100644 --- a/Swiftfin/Components/Slider/CapsuleSlider.swift +++ b/Swiftfin/Components/Slider/CapsuleSlider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/Slider/Slider.swift b/Swiftfin/Components/Slider/Slider.swift index af1077948..dd0a956b0 100644 --- a/Swiftfin/Components/Slider/Slider.swift +++ b/Swiftfin/Components/Slider/Slider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/Slider/ThumbSlider.swift b/Swiftfin/Components/Slider/ThumbSlider.swift index bd30a36f3..3d573ef92 100644 --- a/Swiftfin/Components/Slider/ThumbSlider.swift +++ b/Swiftfin/Components/Slider/ThumbSlider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/SplitContentView.swift b/Swiftfin/Components/SplitContentView.swift index 3cda99250..51c134e93 100644 --- a/Swiftfin/Components/SplitContentView.swift +++ b/Swiftfin/Components/SplitContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/UnmaskSecureField.swift b/Swiftfin/Components/UnmaskSecureField.swift index a1b8015bd..d46860cec 100644 --- a/Swiftfin/Components/UnmaskSecureField.swift +++ b/Swiftfin/Components/UnmaskSecureField.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/UpdateView.swift b/Swiftfin/Components/UpdateView.swift index 8ea47ebfb..8f93a180f 100644 --- a/Swiftfin/Components/UpdateView.swift +++ b/Swiftfin/Components/UpdateView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/Video3DFormatPicker.swift b/Swiftfin/Components/Video3DFormatPicker.swift index cc783d720..f5916cdee 100644 --- a/Swiftfin/Components/Video3DFormatPicker.swift +++ b/Swiftfin/Components/Video3DFormatPicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Components/iOS15View.swift b/Swiftfin/Components/iOS15View.swift index 88cd13ec1..1c2373225 100644 --- a/Swiftfin/Components/iOS15View.swift +++ b/Swiftfin/Components/iOS15View.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/ButtonStyle-iOS.swift b/Swiftfin/Extensions/ButtonStyle-iOS.swift index b582c4634..565dbfcbb 100644 --- a/Swiftfin/Extensions/ButtonStyle-iOS.swift +++ b/Swiftfin/Extensions/ButtonStyle-iOS.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Extensions/Label-iOS.swift b/Swiftfin/Extensions/Label-iOS.swift index 89251c698..02f82ad0d 100644 --- a/Swiftfin/Extensions/Label-iOS.swift +++ b/Swiftfin/Extensions/Label-iOS.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift b/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift index c32f81623..fb24906bf 100644 --- a/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift +++ b/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift index 57e651f96..6ab0cfc7c 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift index 52b1186dd..c6b363bf4 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift index 32606fafe..fe58581b2 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift index 6eed9adf1..0eaa8089f 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift index 5795cea91..2c5469c63 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift index da0797940..24753167b 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/View-iOS.swift b/Swiftfin/Extensions/View/View-iOS.swift index d4d22ad4c..2a64f9936 100644 --- a/Swiftfin/Extensions/View/View-iOS.swift +++ b/Swiftfin/Extensions/View/View-iOS.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Objects/AppURLHandler.swift b/Swiftfin/Objects/AppURLHandler.swift index e66f41c0a..f6ad987da 100644 --- a/Swiftfin/Objects/AppURLHandler.swift +++ b/Swiftfin/Objects/AppURLHandler.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Objects/DeepLink.swift b/Swiftfin/Objects/DeepLink.swift index 526f7b03a..20d226b79 100644 --- a/Swiftfin/Objects/DeepLink.swift +++ b/Swiftfin/Objects/DeepLink.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/AboutAppView.swift b/Swiftfin/Views/AboutAppView.swift index 5ca359acf..fa9b1c2a2 100644 --- a/Swiftfin/Views/AboutAppView.swift +++ b/Swiftfin/Views/AboutAppView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift b/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift index a84054f4f..0c89dd38f 100644 --- a/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift +++ b/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift b/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift index b81799869..f0d313d3e 100644 --- a/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift +++ b/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/ActiveSessionDetailView.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/ActiveSessionDetailView.swift index 4a24205fe..671fa73fd 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/ActiveSessionDetailView.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/ActiveSessionDetailView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/StreamSection.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/StreamSection.swift index 023fb53d4..0e7327abe 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/StreamSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/StreamSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/TranscodeSection.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/TranscodeSection.swift index e1cfdf1eb..3701a48b6 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/TranscodeSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/TranscodeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift index 90735f3ef..e49fdbcae 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionProgressSection.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionProgressSection.swift index e736c298b..f05f61bf4 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionProgressSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionProgressSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionRow.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionRow.swift index 19c97f080..65cfaead1 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift b/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift index cdab09b52..eb72520e5 100644 --- a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift +++ b/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift b/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift index 93d67c701..61a9b4094 100644 --- a/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift +++ b/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift b/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift index 9b992e997..a0ced98a8 100644 --- a/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift +++ b/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift b/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift index 4cf92d50f..ec524a418 100644 --- a/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift +++ b/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift index b24bebd17..71a6000a0 100644 --- a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift +++ b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift index 3e1661b0b..3016443fb 100644 --- a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift +++ b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift index 538bf2f57..a708ab7d0 100644 --- a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift index 2b40e7151..46c79ef8b 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift b/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift index b420e7c0b..202d6b17f 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift +++ b/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift b/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift index b452c5296..e24e050c5 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift index de366c1de..b56799f55 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/DayOfWeekRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/DayOfWeekRow.swift index 0a78f3001..f4b296b85 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/DayOfWeekRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/DayOfWeekRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/IntervalRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/IntervalRow.swift index bb59c35f0..b1d11169f 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/IntervalRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/IntervalRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeLimitSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeLimitSection.swift index 5d73bf487..63073ef93 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeLimitSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeLimitSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeRow.swift index 54b6c5b42..5d630bf50 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TriggerTypeRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TriggerTypeRow.swift index b196ed6e0..2f1ed6139 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TriggerTypeRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TriggerTypeRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/DetailsSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/DetailsSection.swift index 40d868f40..7cfafd7c9 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/DetailsSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/DetailsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastErrorSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastErrorSection.swift index 60ccd9357..59086c4b2 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastErrorSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastErrorSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastRunSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastRunSection.swift index c1c3a32d9..25ad2daab 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastRunSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastRunSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift index 25669db7c..bf9de7f6a 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/TriggersSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/TriggersSection.swift index ed7eb14b5..cc240750e 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/TriggersSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/TriggersSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/TriggerRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/TriggerRow.swift index 433404f7b..538e1e84c 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/TriggerRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/TriggerRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/EditServerTaskView.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/EditServerTaskView.swift index d248e7cb7..1e4c11028 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/EditServerTaskView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/EditServerTaskView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/DestructiveServerTask.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/DestructiveServerTask.swift index 2e35376ef..ced1c6a87 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/DestructiveServerTask.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/DestructiveServerTask.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/ServerTaskRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/ServerTaskRow.swift index db926e33b..11f752732 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/ServerTaskRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/ServerTaskRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/ServerTasksView.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/ServerTasksView.swift index 02e99ab16..61ff42279 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/ServerTasksView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/ServerTasksView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/AddAccessScheduleView/AddAccessScheduleView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/AddAccessScheduleView/AddAccessScheduleView.swift index c88ada76e..1fd9a8c81 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/AddAccessScheduleView/AddAccessScheduleView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/AddAccessScheduleView/AddAccessScheduleView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/Components/EditAccessScheduleRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/Components/EditAccessScheduleRow.swift index a5a362d86..f3ef7541d 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/Components/EditAccessScheduleRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/Components/EditAccessScheduleRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/EditAccessScheduleView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/EditAccessScheduleView.swift index 6e2c27c89..d5c878553 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/EditAccessScheduleView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessSchedule/EditAccessScheduleView/EditAccessScheduleView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift index a31560e29..c65718074 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift index 6298722f8..71e4f96e0 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index 2781c9f7c..68f7e6010 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift index d80b06958..619cb3871 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserParentalRatingView/ServerUserParentalRatingView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserParentalRatingView/ServerUserParentalRatingView.swift index d1518b631..6b5403739 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserParentalRatingView/ServerUserParentalRatingView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserParentalRatingView/ServerUserParentalRatingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift index 099da752f..2a3aef51c 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift index dea82fd35..0b102915f 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift index 4efc03ae3..c3b3b23e9 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift index 39fe395b1..a0383afc0 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift index 99d75e221..e505b6618 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift index 86760e46e..cc32d79df 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift index 0e045ed33..e09354ed7 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift index 8db9c1e9e..e5b5283e9 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift index e08d51a1d..920f18cd5 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift index 84f5d4208..1381ee697 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift index 5dccc1f59..374e72d03 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/AppIconSelectorView.swift b/Swiftfin/Views/AppIconSelectorView.swift index 228e4734a..1c28aa98c 100644 --- a/Swiftfin/Views/AppIconSelectorView.swift +++ b/Swiftfin/Views/AppIconSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AppLoadingView.swift b/Swiftfin/Views/AppLoadingView.swift index 2d78617cb..c3338419c 100644 --- a/Swiftfin/Views/AppLoadingView.swift +++ b/Swiftfin/Views/AppLoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/AppSettingsView/AppSettingsView.swift b/Swiftfin/Views/AppSettingsView/AppSettingsView.swift index 15e89d4b6..f6d3988d0 100644 --- a/Swiftfin/Views/AppSettingsView/AppSettingsView.swift +++ b/Swiftfin/Views/AppSettingsView/AppSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift b/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift index 0350a097c..4e94f2c3a 100644 --- a/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift +++ b/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift b/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift index d9e1e8ea0..2df99f3b0 100644 --- a/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift +++ b/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift b/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift index 8bd058673..b43350563 100644 --- a/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift +++ b/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift b/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift index 93c6ed6f5..672a326c0 100644 --- a/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift +++ b/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ConnectToServerView.swift b/Swiftfin/Views/ConnectToServerView.swift index 23ce660f3..1c9687c94 100644 --- a/Swiftfin/Views/ConnectToServerView.swift +++ b/Swiftfin/Views/ConnectToServerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/DownloadListView.swift b/Swiftfin/Views/DownloadListView.swift index 70f1d5217..7ec2c1268 100644 --- a/Swiftfin/Views/DownloadListView.swift +++ b/Swiftfin/Views/DownloadListView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift b/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift index 207b6b14d..263fe2b72 100644 --- a/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift +++ b/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift b/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift index efafdedb8..3af9d7659 100644 --- a/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift +++ b/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/EditServerView.swift b/Swiftfin/Views/EditServerView.swift index d9f5416c0..2957db3b4 100644 --- a/Swiftfin/Views/EditServerView.swift +++ b/Swiftfin/Views/EditServerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin/Views/FilterView.swift b/Swiftfin/Views/FilterView.swift index f8a937006..2968fb348 100644 --- a/Swiftfin/Views/FilterView.swift +++ b/Swiftfin/Views/FilterView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/FontPickerView.swift b/Swiftfin/Views/FontPickerView.swift index 73a51142d..a14069617 100644 --- a/Swiftfin/Views/FontPickerView.swift +++ b/Swiftfin/Views/FontPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift index 6b3495b41..be52dcc63 100644 --- a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift +++ b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift index a3aa83aeb..0954b31eb 100644 --- a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/HomeView/Components/NextUpView.swift b/Swiftfin/Views/HomeView/Components/NextUpView.swift index 1e33535f1..fd716f262 100644 --- a/Swiftfin/Views/HomeView/Components/NextUpView.swift +++ b/Swiftfin/Views/HomeView/Components/NextUpView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift index e8f3d0587..e0423efe9 100644 --- a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift +++ b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index 64da0366a..564550fd4 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift b/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift index 3f3931e88..07ec23e9d 100644 --- a/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift +++ b/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift index 11edd4665..03223c91f 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift index a08c49a14..669d71687 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift index 3f61017da..a9b42100e 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index f7c85f59d..cb53af0fc 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift index 468fc05ff..77cf97067 100644 --- a/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift index 4f9bc97ce..b4ab8fc28 100644 --- a/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift index 099f7a4a7..267359be0 100644 --- a/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift index 730eb9d62..f1d5df9b8 100644 --- a/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift index cef53b599..ff93e5a0c 100644 --- a/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 316b94f03..3a015b51d 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index 084f77a24..b080227b0 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift index 468fc05ff..77cf97067 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift index 4f9bc97ce..b4ab8fc28 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift index 099f7a4a7..267359be0 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift index 730eb9d62..f1d5df9b8 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift index cef53b599..ff93e5a0c 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift index ac4f54f0d..d70a4e46f 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift index d00c7bc23..2ea9b7204 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift index e42c7b23d..ac73d0be4 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift index c789f0c69..854935c4b 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift index 78b197fbb..675d2dfb6 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift index 560d5412f..c9a580f86 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift index 97998bacf..a8c8f27ec 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift index 9b4be8488..ed5b324eb 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift index 0cb298553..8dbefa43d 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift index 5444f1aab..2789ee25f 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift index aa994e0a4..42f2fc774 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift index 39832ccb9..95c39836c 100644 --- a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemOverviewView.swift b/Swiftfin/Views/ItemOverviewView.swift index d6a60f5ce..aebaaafbe 100644 --- a/Swiftfin/Views/ItemOverviewView.swift +++ b/Swiftfin/Views/ItemOverviewView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift index 78ba39dce..df20f6629 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift index 6829e37b7..710d80df5 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift index 6b5f454b7..27ddd5547 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift index 258e89653..d30c1cc34 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift index 3891700a6..35935d934 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift index c4b4852db..f08905431 100644 --- a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift +++ b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/AttributeHStack.swift b/Swiftfin/Views/ItemView/Components/AttributeHStack.swift index dd019db0f..910fb32b9 100644 --- a/Swiftfin/Views/ItemView/Components/AttributeHStack.swift +++ b/Swiftfin/Views/ItemView/Components/AttributeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift index 9440fe1f0..559140bd4 100644 --- a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift +++ b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift b/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift index fecc6d3d3..5c0d37e43 100644 --- a/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift +++ b/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift index 9db21ebd8..ddd8c62dd 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift index c044dd733..fa60cc94b 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift index f0fb0c4f5..e9eba1035 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index f0a92bf11..bdd29c5af 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift index 4b3083866..30b57b911 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift index a9be264d6..6a9072697 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift index 54fa6b31c..73e6b00dd 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/ItemView/Components/GenresHStack.swift b/Swiftfin/Views/ItemView/Components/GenresHStack.swift index eda8c51b7..842d222b0 100644 --- a/Swiftfin/Views/ItemView/Components/GenresHStack.swift +++ b/Swiftfin/Views/ItemView/Components/GenresHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift b/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift index 8ed6be6a2..1855d8337 100644 --- a/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift +++ b/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/OverviewView.swift b/Swiftfin/Views/ItemView/Components/OverviewView.swift index aed7e679e..c8d6af630 100644 --- a/Swiftfin/Views/ItemView/Components/OverviewView.swift +++ b/Swiftfin/Views/ItemView/Components/OverviewView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/PlayButton.swift b/Swiftfin/Views/ItemView/Components/PlayButton.swift index 70c0dd05a..d23079c1a 100644 --- a/Swiftfin/Views/ItemView/Components/PlayButton.swift +++ b/Swiftfin/Views/ItemView/Components/PlayButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift index 3e99f5508..ec1803676 100644 --- a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift b/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift index fb4638144..7297f3919 100644 --- a/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift index dadee2d12..21794aa7c 100644 --- a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift +++ b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 56416934d..efb194022 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift index e7b9d8cb6..78e4909fb 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift index f0a005dc0..ea873520e 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift index 5b8960278..965c192b1 100644 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift index 1df326d48..0b607492e 100644 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift index 8234dee2f..47211ca3a 100644 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift index 5b9fdfe9b..0e30eccd2 100644 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift index aec4ab53e..c4ae16e34 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift index a7da5a00d..14bfaedcb 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift index 436d2c11c..93fb7c746 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift index 3def39a01..9c33ff874 100644 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift index 436ef3c17..36c771c4f 100644 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift index 91bba8efe..0ac38399f 100644 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift index 9d316e903..4b73b2a87 100644 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift index 613bcfc0b..215062090 100644 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift index bb9914aba..251d74886 100644 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift index a935ff4cb..e5341a8a2 100644 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift index 3dd447847..04fd0ba54 100644 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift index 668ea432c..fe188ebb2 100644 --- a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift index 55113b1b0..13c833de5 100644 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift index e67e4a400..0e73a1dcc 100644 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/MediaSourceInfoView.swift b/Swiftfin/Views/MediaSourceInfoView.swift index d3fd17b03..6f753b684 100644 --- a/Swiftfin/Views/MediaSourceInfoView.swift +++ b/Swiftfin/Views/MediaSourceInfoView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/MediaStreamInfoView.swift b/Swiftfin/Views/MediaStreamInfoView.swift index 4bf560058..ac0f5d27b 100644 --- a/Swiftfin/Views/MediaStreamInfoView.swift +++ b/Swiftfin/Views/MediaStreamInfoView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/MediaView/Components/MediaItem.swift b/Swiftfin/Views/MediaView/Components/MediaItem.swift index 72ccfaeb7..ef336d38f 100644 --- a/Swiftfin/Views/MediaView/Components/MediaItem.swift +++ b/Swiftfin/Views/MediaView/Components/MediaItem.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/MediaView/MediaView.swift b/Swiftfin/Views/MediaView/MediaView.swift index 929a910cc..45b192acc 100644 --- a/Swiftfin/Views/MediaView/MediaView.swift +++ b/Swiftfin/Views/MediaView/MediaView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift b/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift index 00a2020bd..05fa1b2f9 100644 --- a/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift +++ b/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift b/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift index 18e63e6e6..e0a5999b6 100644 --- a/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift +++ b/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift index b2e63a52b..1198d1cfd 100644 --- a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift b/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift index f0d9bf967..5d65d1029 100644 --- a/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift +++ b/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift b/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift index 4208ea66f..a1adf0eed 100644 --- a/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift +++ b/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ProgramsView/ProgramsView.swift b/Swiftfin/Views/ProgramsView/ProgramsView.swift index acb86de55..f4c44ca95 100644 --- a/Swiftfin/Views/ProgramsView/ProgramsView.swift +++ b/Swiftfin/Views/ProgramsView/ProgramsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/QuickConnectView.swift b/Swiftfin/Views/QuickConnectView.swift index 0260de765..3ceb286a5 100644 --- a/Swiftfin/Views/QuickConnectView.swift +++ b/Swiftfin/Views/QuickConnectView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift b/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift index 9a5c0d296..2aeca1f1a 100644 --- a/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift +++ b/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/SearchView.swift b/Swiftfin/Views/SearchView.swift index c2f97b196..a5d047d3b 100644 --- a/Swiftfin/Views/SearchView.swift +++ b/Swiftfin/Views/SearchView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift b/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift index a0145a481..c5686602c 100644 --- a/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift +++ b/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections diff --git a/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift b/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift index 336d20993..122c30989 100644 --- a/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift +++ b/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections diff --git a/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift b/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift index ee370848e..38b648341 100644 --- a/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift +++ b/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift b/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift index e3c8d3cc0..19485597a 100644 --- a/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift +++ b/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SelectUserView/Components/UserRow.swift b/Swiftfin/Views/SelectUserView/Components/UserRow.swift index 4752108d7..c5bc65d92 100644 --- a/Swiftfin/Views/SelectUserView/Components/UserRow.swift +++ b/Swiftfin/Views/SelectUserView/Components/UserRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SelectUserView/SelectUserView.swift b/Swiftfin/Views/SelectUserView/SelectUserView.swift index bfdbeb57b..19a2b3669 100644 --- a/Swiftfin/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin/Views/SelectUserView/SelectUserView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/ServerCheckView.swift b/Swiftfin/Views/ServerCheckView.swift index d924c9ace..3ece182f5 100644 --- a/Swiftfin/Views/ServerCheckView.swift +++ b/Swiftfin/Views/ServerCheckView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift index 7e1d630bc..0060044a2 100644 --- a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift +++ b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift index 474992ba4..b66cf2bf8 100644 --- a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift +++ b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift index 45e5c2962..b7490a771 100644 --- a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift +++ b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index 8c1b5bdf0..bc66cbb10 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift index 0e0f730f9..0e08a81c5 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift index 4bede18b5..cbb8d1b44 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/DebugSettingsView.swift b/Swiftfin/Views/SettingsView/DebugSettingsView.swift index 4abdf4acf..ddf2c1316 100644 --- a/Swiftfin/Views/SettingsView/DebugSettingsView.swift +++ b/Swiftfin/Views/SettingsView/DebugSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift index 162f984e4..9101f4d3b 100644 --- a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/GestureSettingsView.swift b/Swiftfin/Views/SettingsView/GestureSettingsView.swift index 2c6a41aa8..a84e816a0 100644 --- a/Swiftfin/Views/SettingsView/GestureSettingsView.swift +++ b/Swiftfin/Views/SettingsView/GestureSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift b/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift index b4c74f89d..5cbea284e 100644 --- a/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift +++ b/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift b/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift index a6458bc93..12ff5e561 100644 --- a/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift +++ b/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift b/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift index 1c003daf4..3beaed2dd 100644 --- a/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift +++ b/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift b/Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift index a3335d2f2..c43d5a3c9 100644 --- a/Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift +++ b/Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift index d0990d916..d87d0b8ed 100644 --- a/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift index a7af01cc0..b15a5e0a0 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift index 7c33f5313..703e8cf9d 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift index 87b1c5889..f7bf18b9b 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift index fc92ba9ef..27f84cda2 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift index 05e2f6ee1..31dcb1913 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift index e70365ad8..cfe89cc40 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift index 3a4f4d589..2239aa189 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift index 3ad3a5479..4597464be 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift index 3c307a527..e4ba36c75 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift index 885f046a8..2ab198055 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift b/Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift index 2f42dc85f..358d162db 100644 --- a/Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift +++ b/Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import PhotosUI diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift b/Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift index 709fc5a35..3b649b174 100644 --- a/Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift +++ b/Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePicker.swift b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePicker.swift index dd9130340..6ebc6875e 100644 --- a/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePicker.swift +++ b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift b/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift index 6a22cbd86..9e41443a5 100644 --- a/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift +++ b/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift b/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift index bb680fdb1..a431ea77e 100644 --- a/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift +++ b/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/UserSignInView/UserSignInView.swift b/Swiftfin/Views/UserSignInView/UserSignInView.swift index 7be62cadd..aa8372379 100644 --- a/Swiftfin/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView/UserSignInView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift index 90cbfb6be..74391c0a9 100644 --- a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift b/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift index 33ad6e9f6..89194f41a 100644 --- a/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift +++ b/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift b/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift index b3a0e85a4..4edd47a46 100644 --- a/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift index 080e8d610..d7c572d84 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift index 9e46aa653..8659991e7 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift index d7861d6d5..0c2b5aaa0 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift index d7f01388f..3c619c6bc 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift index ca54557f2..a6b425034 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift index a013ec8de..2cd469308 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift b/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift index 1f505d744..32a6be53b 100644 --- a/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift b/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift index b64449e30..d6f940e7d 100644 --- a/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift index 31b0969c7..b53bfb308 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift index 3d81ae007..b235e2a92 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift index 0be7b9388..d5a40d569 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift index 77eb23539..ad9ac8ccc 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift index 1f878bdc2..3bb217e7a 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift index 7c0cb3e8e..308383de8 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift index e3543f3db..c842ad643 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift index cf1034606..cb8d30b0f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift index 2e2515c92..3816c4a1b 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift index 1956fd033..673be6674 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift index fbfb98f3f..7829c9cd6 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift index d3d44d35d..1fe8861ff 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift index b333ee41b..d98c86e2f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift index 8f89e3156..d34cf044d 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift index 03faba09d..73cdee522 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift index 99d4eda86..8a7f69335 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift index f56baccf3..9bb3ac8d6 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift index 649c0a20c..f84baf46f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift index fd76d5cf6..0d99aaa12 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift index 96518f99a..6e6c825db 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift index 585d24c06..985767409 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift index b5315fb8e..a6089fe3f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift b/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift index 466917b02..cbb020194 100644 --- a/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift +++ b/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift b/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift index 19a58df96..5d6a0d4ce 100644 --- a/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift +++ b/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/VideoPlayer.swift b/Swiftfin/Views/VideoPlayer/VideoPlayer.swift index bf3e46cd1..800966ef3 100644 --- a/Swiftfin/Views/VideoPlayer/VideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/VideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults From 7baead409870723a8164cf4b1b0965b8b0fd5d26 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 3 Jan 2025 18:04:37 -0700 Subject: [PATCH 16/45] Uploading finally works! --- .../JellyfinAPI/RemoteImageInfo.swift | 10 ++- Shared/Strings/Strings.swift | 4 + .../ItemImagesViewModel.swift | 85 ++++++++++--------- .../EditItemImagesView.swift | 44 +++++++--- Translations/en.lproj/Localizable.strings | 6 ++ 5 files changed, 92 insertions(+), 57 deletions(-) diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift index 4d20aef40..2b9b091a5 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift @@ -10,9 +10,13 @@ import Foundation import JellyfinAPI import SwiftUI -extension RemoteImageInfo: @retroactive Identifiable { +extension RemoteImageInfo: @retroactive Identifiable, LibraryIdentifiable { - public var id: String { - UUID().uuidString + var unwrappedIDHashOrZero: Int { + id.hashValue + } + + public var id: Int { + self.hashValue } } diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 44e396617..f5bc7d71d 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -1324,6 +1324,10 @@ internal enum L10n { internal static let unreleased = L10n.tr("Localizable", "unreleased", fallback: "Unreleased") /// You have unsaved changes. Are you sure you want to discard them? internal static let unsavedChangesMessage = L10n.tr("Localizable", "unsavedChangesMessage", fallback: "You have unsaved changes. Are you sure you want to discard them?") + /// Upload file + internal static let uploadFile = L10n.tr("Localizable", "uploadFile", fallback: "Upload file") + /// Upload photo + internal static let uploadPhoto = L10n.tr("Localizable", "uploadPhoto", fallback: "Upload photo") /// URL internal static let url = L10n.tr("Localizable", "url", fallback: "URL") /// Use as Transcoding Profile diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index e9f43f62d..d77fd2165 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -76,19 +76,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { self.item = item self.includeAllLanguages = includeAllLanguages super.init() - - Notifications[.itemMetadataDidChange] - .publisher - .sink { [weak self] item in - guard let self else { return } - self.item = item - Task { - await MainActor.run { - self.send(.backgroundRefresh) - } - } - } - .store(in: &cancellables) } // MARK: - Respond to Actions @@ -145,7 +132,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { await MainActor.run { self.state = .error(apiError) self.eventSubject.send(.error(apiError)) - _ = self.backgroundStates.remove(.refreshing) + self.backgroundStates.remove(.refreshing) } } }.asAnyCancellable() @@ -159,20 +146,21 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { guard let self = self else { return } do { await MainActor.run { - _ = self.state = .updating + self.state = .updating } try await self.uploadImage(url, type: imageType) + try await self.refreshItem() await MainActor.run { + self.state = .content self.eventSubject.send(.updated) - _ = self.state = .updating } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { + self.state = .error(apiError) self.eventSubject.send(.error(apiError)) - _ = self.state = .updating } } }.asAnyCancellable() @@ -186,20 +174,21 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { guard let self = self else { return } do { await MainActor.run { - _ = self.state = .deleting + self.state = .deleting } - try await self.deleteImage(imageInfo) + try await deleteImage(imageInfo) + try await refreshItem() await MainActor.run { self.eventSubject.send(.deleted) - _ = self.state = .deleting + self.state = .deleting } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { + self.state = .deleting self.eventSubject.send(.error(apiError)) - _ = self.state = .deleting } } }.asAnyCancellable() @@ -213,27 +202,21 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { private func getAllImages() async throws { guard let itemID = item.id else { return } - // Get all of the ImageInfos for the Item let imageRequest = Paths.getItemImageInfos(itemID: itemID) let imageResponse = try await self.userSession.client.send(imageRequest) let imageInfos = imageResponse.value - // Get Current vs New ImageInfos for comparison let currentImageInfos = Set(self.images.keys) let newImageInfos = Set(imageInfos) - // Exit if all ImageInfos are the same guard currentImageInfos != newImageInfos else { return } - // Remove missing ImageInfos from published Images await MainActor.run { self.images = self.images.filter { newImageInfos.contains($0.key) } } - // Identify missing ImageInfos in the published Images let missingImageInfos = imageInfos.filter { !self.images.keys.contains($0) } - // Get all UIImages for all missing ImageInfos try await withThrowingTaskGroup(of: (ImageInfo, UIImage).self) { group in for imageInfo in missingImageInfos { group.addTask { @@ -249,7 +232,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { ) let response = try await self.userSession.client.send(request) - // Convert the Response Data into a UIImage if let image = UIImage(data: response.value) { return (imageInfo, image) } @@ -260,7 +242,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } } - // Publish ImageInfos for try await (imageInfo, image) in group { await MainActor.run { self.images[imageInfo] = image @@ -271,30 +252,35 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { // MARK: - Upload Image - // TODO: Make actually work. 500 error. Bad format. private func uploadImage(_ url: URL, type: ImageType) async throws { guard let itemID = item.id else { return } - // Start accessing security-scoped resource guard url.startAccessingSecurityScopedResource() else { throw JellyfinAPIError("Unable to access file at \(url)") } defer { url.stopAccessingSecurityScopedResource() } let data = try Data(contentsOf: url) - let image = UIImage(data: data)! + guard let image = UIImage(data: data) else { + throw JellyfinAPIError("Unable to load image from file") + } + + let maxDimension: CGFloat = 3000 + let resizedImage = image.size.width > maxDimension || image.size.height > maxDimension + ? resizeImage(image, maxDimension: maxDimension) + : image + let contentType: String let imageData: Data - if let pngData = image.pngData() { + if let pngData = resizedImage.pngData() { contentType = "image/png" - imageData = pngData - } else if let jpgData = image.jpegData(compressionQuality: 1) { + imageData = pngData.base64EncodedData() + } else if let jpgData = resizedImage.jpegData(compressionQuality: 0.8) { contentType = "image/jpeg" - imageData = jpgData + imageData = jpgData.base64EncodedData() } else { - logger.error("Unable to convert given profile image to png/jpg") - throw JellyfinAPIError("An internal error occurred") + throw JellyfinAPIError("Failed to convert image to png/jpg") } var request = Paths.setItemImage( @@ -305,8 +291,27 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { request.headers = ["Content-Type": contentType] _ = try await userSession.client.send(request) + } + + // MARK: - Resize Large Image + + private func resizeImage(_ image: UIImage, maxDimension: CGFloat) -> UIImage { + let size = image.size + let aspectRatio = size.width / size.height - try await refreshItem() + var newSize: CGSize + if size.width > size.height { + newSize = CGSize(width: maxDimension, height: maxDimension / aspectRatio) + } else { + newSize = CGSize(width: maxDimension * aspectRatio, height: maxDimension) + } + + UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) + image.draw(in: CGRect(origin: .zero, size: newSize)) + let resizedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return resizedImage ?? image } // MARK: - Delete Image @@ -324,8 +329,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { _ = try await userSession.client.send(request) - try await refreshItem() - await MainActor.run { self.images.removeValue(forKey: imageInfo) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index b080227b0..36d9ac73e 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -135,23 +135,41 @@ struct EditItemImagesView: View { private func sectionHeader(for imageType: ImageType) -> some View { HStack(alignment: .center, spacing: 16) { Text(imageType.rawValue.localizedCapitalized) + .font(.headline) Spacer() - Button(action: { - router.route( - to: \.addImage, - RemoteImageInfoViewModel( - item: viewModel.item, - imageType: imageType + Menu(L10n.options, systemImage: "plus") { + Button(action: { + router.route( + to: \.addImage, + RemoteImageInfoViewModel( + item: viewModel.item, + imageType: imageType + ) ) - ) - }) { - Image(systemName: "magnifyingglass") - } - Button(action: { uploadType = imageType }) { - Image(systemName: "plus") + }) { + Label(L10n.search, systemImage: "magnifyingglass") + } + + Divider() + + Button(action: { + uploadType = imageType + }) { + Label(L10n.uploadFile, systemImage: "document.badge.plus") + } + + Button(action: { + uploadType = imageType + }) { + Label(L10n.uploadPhoto, systemImage: "photo.badge.plus") + } } + .font(.body) + .labelStyle(.iconOnly) + .backport + .fontWeight(.semibold) + .foregroundStyle(accentColor) } - .font(.headline) .padding(.horizontal, 30) } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 2b2557e8f..df3160210 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1897,6 +1897,12 @@ /// You have unsaved changes. Are you sure you want to discard them? "unsavedChangesMessage" = "You have unsaved changes. Are you sure you want to discard them?"; +/// Upload file +"uploadFile" = "Upload file"; + +/// Upload photo +"uploadPhoto" = "Upload photo"; + /// URL "url" = "URL"; From c03f2c1dabd86cd097ecb224e6954b023462057e Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 3 Jan 2025 19:46:37 -0700 Subject: [PATCH 17/45] Functional but messy. TODO: - Figure out better resizing if too big? - Upload from Photos - Move upload logic to imageViewModel and make RemtoeImageViewModel PagingLibraryViewModel conformant - Create a ImageInfoView for Selection & Deletion. --- Shared/Extensions/JellyfinAPI/ImageInfo.swift | 2 +- .../JellyfinAPI/RemoteImageInfo.swift | 6 +-- .../ItemImagesViewModel.swift | 41 ++++++++++++++----- .../RemoteImageInfoViewModel.swift | 1 - Swiftfin.xcodeproj/project.pbxproj | 1 - .../EditItemImagesView.swift | 6 ++- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/Shared/Extensions/JellyfinAPI/ImageInfo.swift b/Shared/Extensions/JellyfinAPI/ImageInfo.swift index 1560e7df2..8e2d3e958 100644 --- a/Shared/Extensions/JellyfinAPI/ImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ImageInfo.swift @@ -12,6 +12,6 @@ import JellyfinAPI // TODO: How SHOULD I get Identifiable extension ImageInfo: @retroactive Identifiable { public var id: String { - self.imageTag?.hashString ?? UUID().uuidString + self.hashString } } diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift index 2b9b091a5..8a59b9fe7 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift @@ -10,11 +10,7 @@ import Foundation import JellyfinAPI import SwiftUI -extension RemoteImageInfo: @retroactive Identifiable, LibraryIdentifiable { - - var unwrappedIDHashOrZero: Int { - id.hashValue - } +extension RemoteImageInfo: @retroactive Identifiable { public var id: Int { self.hashValue diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index d77fd2165..9bd02b333 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -76,6 +76,19 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { self.item = item self.includeAllLanguages = includeAllLanguages super.init() + + Notifications[.itemMetadataDidChange] + .publisher + .sink { [weak self] item in + guard let self else { return } + self.item = item + Task { + await MainActor.run { + self.send(.backgroundRefresh) + } + } + } + .store(in: &cancellables) } // MARK: - Respond to Actions @@ -276,7 +289,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { if let pngData = resizedImage.pngData() { contentType = "image/png" imageData = pngData.base64EncodedData() - } else if let jpgData = resizedImage.jpegData(compressionQuality: 0.8) { + } else if let jpgData = resizedImage.jpegData(compressionQuality: 1) { contentType = "image/jpeg" imageData = jpgData.base64EncodedData() } else { @@ -318,16 +331,24 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { private func deleteImage(_ imageInfo: ImageInfo) async throws { guard let itemID = item.id, - let imageType = imageInfo.imageType?.rawValue, - let imageIndex = imageInfo.imageIndex else { return } + let imageType = imageInfo.imageType?.rawValue else { return } - let request = Paths.deleteItemImageByIndex( - itemID: itemID, - imageType: imageType, - imageIndex: imageIndex - ) + if let imageIndex = imageInfo.imageIndex { + let request = Paths.deleteItemImageByIndex( + itemID: itemID, + imageType: imageType, + imageIndex: imageIndex + ) - _ = try await userSession.client.send(request) + try await userSession.client.send(request) + } else { + let request = Paths.deleteItemImage( + itemID: itemID, + imageType: imageType + ) + + try await userSession.client.send(request) + } await MainActor.run { self.images.removeValue(forKey: imageInfo) @@ -376,7 +397,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { await MainActor.run { self.item = response.value _ = backgroundStates.remove(.refreshing) - Notifications[.itemMetadataDidChange].post(item) + // Notifications[.itemMetadataDidChange].post(item) } } } diff --git a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift index be519c389..ffce0ac37 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift @@ -8,7 +8,6 @@ import Combine import Foundation -import Get import IdentifiedCollections import JellyfinAPI import OrderedCollections diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 84a3db6ae..ccc823231 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2570,7 +2570,6 @@ 4E37F6192D17EB3C0022AADD /* ItemMetadata */, 4EE766F32D131F6E009658F0 /* IdentifyItemView */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, - 4E3766192D2144BA00C5D7A5 /* ItemElements */, ); path = ItemEditorView; sourceTree = ""; diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index 36d9ac73e..7ba23f3c2 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -117,9 +117,13 @@ struct EditItemImagesView: View { let imageArray = Array(filteredImages) if !imageArray.isEmpty { + let sortedImageArray = imageArray.sorted { lhs, rhs in + (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) + } + ScrollView(.horizontal, showsIndicators: false) { HStack { - ForEach(imageArray, id: \.key) { imageData in + ForEach(sortedImageArray, id: \.key) { imageData in imageButton(imageData.value) { selectedImage = imageData.key } From 605eee621b1bf6ce4301f16beb6928a6d2e4ab85 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 4 Jan 2025 15:44:03 -0700 Subject: [PATCH 18/45] Now conforms to PagingLIbraryViewModel but everything else is a mess --- .../Coordinators/ItemEditorCoordinator.swift | 2 +- .../JellyfinAPI/RemoteImageInfo.swift | 18 +- Shared/Extensions/JellyfinAPI/Request.swift | 66 ++++++ .../ItemImagesViewModel.swift | 163 +++++++++----- .../RemoteImageInfoViewModel.swift | 213 +----------------- .../UserProfileImageViewModel.swift | 6 + Swiftfin.xcodeproj/project.pbxproj | 6 + .../AddItemImageView/AddItemImageView.swift | 46 ++-- .../EditItemImagesView.swift | 21 +- 9 files changed, 248 insertions(+), 293 deletions(-) create mode 100644 Shared/Extensions/JellyfinAPI/Request.swift diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index faeb4963e..166032301 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -87,7 +87,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { EditItemImagesView(viewModel: ItemImagesViewModel(item: item)) } - func makeAddImage(viewModel: RemoteImageInfoViewModel) -> some View { + func makeAddImage(viewModel: ItemImagesViewModel) -> some View { AddItemImageView(viewModel: viewModel) } diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift index 8a59b9fe7..e308e1861 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift @@ -10,7 +10,23 @@ import Foundation import JellyfinAPI import SwiftUI -extension RemoteImageInfo: @retroactive Identifiable { +extension RemoteImageInfo: @retroactive Identifiable, Poster { + + var displayTitle: String { + self.providerName ?? L10n.unknown + } + + var unwrappedIDHashOrZero: Int { + self.id + } + + var subtitle: String? { + self.language + } + + var systemImage: String { + "circle" + } public var id: Int { self.hashValue diff --git a/Shared/Extensions/JellyfinAPI/Request.swift b/Shared/Extensions/JellyfinAPI/Request.swift new file mode 100644 index 000000000..4f8585eac --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/Request.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) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import Get + +public extension Request { + /// Size of the request HTTP method in bytes + var methodSize: Int { + method.rawValue.count + } + + /// Size of the request URL in bytes + var urlSize: Int { + url?.absoluteString.count ?? 0 + } + + /// Size of the request query parameters in bytes + var querySize: Int { + guard let query = query else { return 0 } + return query.reduce(0) { $0 + $1.0.count + ($1.1?.count ?? 0) + 2 } + } + + /// Size of the request headers in bytes + var headersSize: Int { + guard let headers = headers else { return 0 } + return headers.reduce(0) { $0 + $1.key.count + $1.value.count + 4 } + } + + /// Size of the request body in bytes + var bodySize: Int { + var size = 0 + if let body = body { + do { + let bodyData = try JSONEncoder().encode(AnyEncodable(body)) + size += bodyData.count + } catch { + size += 0 + } + } + return size + } + + /// Total size of the total request in bytes + var requestSize: Int { + methodSize + urlSize + querySize + headersSize + bodySize + } +} + +/// A type-erased `Encodable` to encode any value conforming to `Encodable` +private struct AnyEncodable: Encodable { + private let _encode: (Encoder) throws -> Void + + init(_ value: T) { + _encode = value.encode + } + + func encode(to encoder: Encoder) throws { + try _encode(encoder) + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 9bd02b333..48d3efe37 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -12,6 +12,7 @@ import IdentifiedCollections import JellyfinAPI import OrderedCollections import SwiftUI +import UniformTypeIdentifiers class ItemImagesViewModel: ViewModel, Stateful, Eventful { @@ -22,9 +23,12 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } enum Action: Equatable { + case cancel case refresh case backgroundRefresh - case uploadImage(url: URL, type: ImageType) + case selectType(ImageType?) + case setImage(RemoteImageInfo) + case uploadImage(URL) case deleteImage(ImageInfo) } @@ -49,6 +53,8 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { @Published var item: BaseItemDto @Published + var selectedType: ImageType? + @Published var images: [ImageInfo: UIImage] = [:] // MARK: - State Management @@ -76,19 +82,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { self.item = item self.includeAllLanguages = includeAllLanguages super.init() - - Notifications[.itemMetadataDidChange] - .publisher - .sink { [weak self] item in - guard let self else { return } - self.item = item - Task { - await MainActor.run { - self.send(.backgroundRefresh) - } - } - } - .store(in: &cancellables) } // MARK: - Respond to Actions @@ -96,6 +89,19 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { func respond(to action: Action) -> State { switch action { + case .cancel: + task?.cancel() + self.state = .initial + + return state + + case let .selectType(type): + task?.cancel() + + self.selectedType = type + + return state + case .backgroundRefresh: task?.cancel() @@ -152,7 +158,34 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .uploadImage(url, imageType): + case let .setImage(remoteImageInfo): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.state = .updating + } + + try await self.setImage(remoteImageInfo) + + await MainActor.run { + self.eventSubject.send(.updated) + _ = self.state = .updating + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.eventSubject.send(.error(apiError)) + _ = self.state = .updating + } + } + }.asAnyCancellable() + + return state + + case let .uploadImage(url): task?.cancel() task = Task { [weak self] in @@ -162,7 +195,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { self.state = .updating } - try await self.uploadImage(url, type: imageType) + try await self.uploadImage(url) try await self.refreshItem() await MainActor.run { @@ -263,68 +296,89 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } } + // MARK: - Set Image From URL + + private func setImage(_ remoteImageInfo: RemoteImageInfo) async throws { + guard let itemID = item.id, + let type = remoteImageInfo.type, + let imageURL = remoteImageInfo.url else { return } + + let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: imageURL) + let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) + try await userSession.client.send(imageRequest) + + await MainActor.run { + self.selectedType = nil + } + } + // MARK: - Upload Image - private func uploadImage(_ url: URL, type: ImageType) async throws { - guard let itemID = item.id else { return } + private func uploadImage(_ url: URL) async throws { + guard let itemID = item.id, + let type = selectedType else { return } guard url.startAccessingSecurityScopedResource() else { throw JellyfinAPIError("Unable to access file at \(url)") } defer { url.stopAccessingSecurityScopedResource() } - let data = try Data(contentsOf: url) - guard let image = UIImage(data: data) else { - throw JellyfinAPIError("Unable to load image from file") - } + let fileData = try Data(contentsOf: url) + let fileSize = fileData.count - let maxDimension: CGFloat = 3000 - let resizedImage = image.size.width > maxDimension || image.size.height > maxDimension - ? resizeImage(image, maxDimension: maxDimension) - : image + guard fileSize < 30_000_000 else { + throw JellyfinAPIError( + "This image is too large (\(fileSize.formatted(.byteCount(style: .file)))). The upload limit for images is 30 MB." + ) + } let contentType: String let imageData: Data - if let pngData = resizedImage.pngData() { - contentType = "image/png" - imageData = pngData.base64EncodedData() - } else if let jpgData = resizedImage.jpegData(compressionQuality: 1) { - contentType = "image/jpeg" - imageData = jpgData.base64EncodedData() + if let fileType = UTType(filenameExtension: url.pathExtension) { + if fileType.conforms(to: .png) { + contentType = "image/png" + imageData = fileData + } else if fileType.conforms(to: .jpeg) { + contentType = "image/jpeg" + imageData = fileData + } else { + guard let image = UIImage(data: fileData) else { + throw JellyfinAPIError("Unable to load image from file") + } + + if let pngData = image.pngData() { + contentType = "image/png" + imageData = pngData + } else if let jpgData = image.jpegData(compressionQuality: 1) { + contentType = "image/jpeg" + imageData = jpgData + } else { + throw JellyfinAPIError("Failed to convert image to png/jpg") + } + } } else { - throw JellyfinAPIError("Failed to convert image to png/jpg") + throw JellyfinAPIError("Unsupported file type") } var request = Paths.setItemImage( itemID: itemID, imageType: type.rawValue, - imageData + imageData.base64EncodedData() ) request.headers = ["Content-Type": contentType] - _ = try await userSession.client.send(request) - } - - // MARK: - Resize Large Image - - private func resizeImage(_ image: UIImage, maxDimension: CGFloat) -> UIImage { - let size = image.size - let aspectRatio = size.width / size.height - - var newSize: CGSize - if size.width > size.height { - newSize = CGSize(width: maxDimension, height: maxDimension / aspectRatio) - } else { - newSize = CGSize(width: maxDimension * aspectRatio, height: maxDimension) + guard request.bodySize < 30_000_000 else { + throw JellyfinAPIError( + "Converting this image into a valid format has resulted in a larger image (\(request.bodySize.formatted(.byteCount(style: .file)))) that is now too large for upload. Before conversion this image was \(fileData.count.formatted(.byteCount(style: .file))). The upload limit for images is 30 MB." + ) } - UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) - image.draw(in: CGRect(origin: .zero, size: newSize)) - let resizedImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() + _ = try await userSession.client.send(request) - return resizedImage ?? image + await MainActor.run { + self.selectedType = nil + } } // MARK: - Delete Image @@ -374,6 +428,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { result[updatedInfo] = pair.value } + self.selectedType = nil self.images = updatedImages } } @@ -397,7 +452,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { await MainActor.run { self.item = response.value _ = backgroundStates.remove(.refreshing) - // Notifications[.itemMetadataDidChange].post(item) + Notifications[.itemMetadataDidChange].post(item) } } } diff --git a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift index ffce0ac37..6eef42471 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift @@ -12,70 +12,19 @@ import IdentifiedCollections import JellyfinAPI import OrderedCollections import UIKit -import URLQueryEncoder private let DefaultPageSize = 50 -class RemoteImageInfoViewModel: ViewModel, Stateful { - - enum Event: Equatable { - case updated - case error(JellyfinAPIError) - } - - enum Action: Equatable { - case cancel - case refresh - case getNextPage - case setImage(url: String, type: ImageType) - } - - enum BackgroundState: Hashable { - case gettingNextPage - case refreshing - } - - enum State: Hashable { - case initial - case content - case updating - case error(JellyfinAPIError) - } - - // MARK: - Published Variables +class RemoteImageInfoViewModel: PagingLibraryViewModel { @Published var item: BaseItemDto + @Published var imageType: ImageType - @Published - var includeAllLanguages: Bool - @Published - var images: IdentifiedArrayOf = [] - - // MARK: - Page Management - - private let pageSize: Int - private(set) var currentPage: Int = 0 - private(set) var hasNextPage: Bool = true - // MARK: - State Management - - @Published - var state: State = .initial @Published - var backgroundStates: OrderedSet = [] - - private var task: AnyCancellable? - private let eventSubject = PassthroughSubject() - - // MARK: - Eventful - - var events: AnyPublisher { - eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher() - } - - // MARK: - Initializer + var includeAllLanguages: Bool init( item: BaseItemDto, @@ -86,167 +35,23 @@ class RemoteImageInfoViewModel: ViewModel, Stateful { self.item = item self.imageType = imageType self.includeAllLanguages = includeAllLanguages - self.pageSize = pageSize - super.init() - } - - // MARK: - Respond to Actions - - func respond(to action: Action) -> State { - switch action { - - case .cancel: - task?.cancel() - self.state = .initial - - return state - - case .refresh: - task?.cancel() - - task = Task { [weak self] in - guard let self else { return } - do { - await MainActor.run { - self.state = .initial - self.images.removeAll() - self.currentPage = 0 - self.hasNextPage = true - _ = self.backgroundStates.append(.refreshing) - } - - try await self.getNextPage(imageType) - - await MainActor.run { - self.state = .content - _ = self.backgroundStates.remove(.refreshing) - } - } catch { - let apiError = JellyfinAPIError(error.localizedDescription) - await MainActor.run { - self.state = .error(apiError) - _ = self.backgroundStates.remove(.refreshing) - } - } - }.asAnyCancellable() - - return state - - case .getNextPage: - guard hasNextPage else { return .content } - - task?.cancel() - - task = Task { [weak self] in - guard let self else { return } - do { - await MainActor.run { - _ = self.backgroundStates.append(.gettingNextPage) - } - - try await self.getNextPage(imageType) - - await MainActor.run { - self.state = .content - _ = self.backgroundStates.remove(.gettingNextPage) - } - } catch { - let apiError = JellyfinAPIError(error.localizedDescription) - await MainActor.run { - self.state = .error(apiError) - _ = self.backgroundStates.remove(.gettingNextPage) - } - } - }.asAnyCancellable() - - return state - - case let .setImage(url, imageType): - task?.cancel() - - task = Task { [weak self] in - guard let self = self else { return } - do { - await MainActor.run { - _ = self.state = .updating - } - - try await self.setImage(url, type: imageType) - - await MainActor.run { - self.eventSubject.send(.updated) - _ = self.state = .updating - } - } catch { - let apiError = JellyfinAPIError(error.localizedDescription) - await MainActor.run { - self.eventSubject.send(.error(apiError)) - _ = self.state = .updating - } - } - }.asAnyCancellable() - - return state - } + super.init(parent: nil, pageSize: pageSize) } - // MARK: - Get Next Page - - private func getNextPage(_ type: ImageType) async throws { - guard let itemID = item.id, hasNextPage else { return } + override func get(page: Int) async throws -> [RemoteImageInfo] { + guard let itemID = item.id else { return [] } - let startIndex = currentPage * pageSize + let startIndex = page * pageSize let parameters = Paths.GetRemoteImagesParameters( - type: type, + type: imageType, startIndex: startIndex, limit: pageSize, isIncludeAllLanguages: includeAllLanguages ) let request = Paths.getRemoteImages(itemID: itemID, parameters: parameters) - let response = try await userSession.client.send(request) - let newImages = response.value.images ?? [] - - hasNextPage = newImages.count >= pageSize - - await MainActor.run { - self.images.append(contentsOf: newImages) - currentPage += 1 - } - } - - // MARK: - Set Image From URL - - private func setImage(_ url: String, type: ImageType) async throws { - guard let itemID = item.id else { return } - - let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: url) - let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) - try await userSession.client.send(imageRequest) - - try await refreshItem() - } - - // MARK: - Refresh Item - - private func refreshItem() async throws { - guard let itemID = item.id else { return } - - await MainActor.run { - _ = 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 - _ = backgroundStates.remove(.refreshing) - Notifications[.itemMetadataDidChange].post(item) - } + return response.value.images ?? [] } } diff --git a/Shared/ViewModels/UserProfileImageViewModel.swift b/Shared/ViewModels/UserProfileImageViewModel.swift index 2fbfe7626..aa49eb826 100644 --- a/Shared/ViewModels/UserProfileImageViewModel.swift +++ b/Shared/ViewModels/UserProfileImageViewModel.swift @@ -151,6 +151,12 @@ class UserProfileImageViewModel: ViewModel, Eventful, Stateful { ) request.headers = ["Content-Type": contentType] + guard imageData.count > 30_000_000 else { + throw JellyfinAPIError( + "This profile image is too large (\(imageData.count.formatted(.byteCount(style: .file)))). The upload limit for images is 30 MB." + ) + } + let _ = try await userSession.client.send(request) sweepProfileImageCache() diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index ccc823231..5bc00cfde 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -184,6 +184,8 @@ 4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */; }; 4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */; }; 4EA397472CD31CC000904C25 /* AddServerUserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */; }; + 4EA78B0F2D29B0880093BFCE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B0E2D29B0820093BFCE /* Request.swift */; }; + 4EA78B102D29B0880093BFCE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B0E2D29B0820093BFCE /* Request.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; 4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; }; 4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; }; @@ -1332,6 +1334,7 @@ 4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysView.swift; sourceTree = ""; }; 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysRow.swift; sourceTree = ""; }; 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserViewModel.swift; sourceTree = ""; }; + 4EA78B0E2D29B0820093BFCE /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = ""; }; 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = ""; }; 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = ""; }; @@ -4534,6 +4537,7 @@ 4E2182E42CAF67EF0094806B /* PlayMethod.swift */, 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */, 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */, + 4EA78B0E2D29B0820093BFCE /* Request.swift */, 4E35CE652CBED8B300DBD886 /* ServerTicks.swift */, 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */, 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */, @@ -5628,6 +5632,7 @@ E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */, E154967C296CBB1A00C4EF88 /* FontPickerView.swift in Sources */, E15D63F02BD6DFC200AA665D /* SystemImageable.swift in Sources */, + 4EA78B102D29B0880093BFCE /* Request.swift in Sources */, E1EA096A2BED78F5004CDE76 /* UserAccessPolicy.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, E1575E86293E7A00001665B1 /* AppIcons.swift in Sources */, @@ -5890,6 +5895,7 @@ E113132B28BDB4B500930F75 /* NavigationBarDrawerView.swift in Sources */, E164A7F62BE4814700A54B18 /* SelectUserServerSelection.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerConnectionViewModel.swift in Sources */, + 4EA78B0F2D29B0880093BFCE /* Request.swift in Sources */, E1EA09882BEE9CF3004CDE76 /* UserLocalSecurityView.swift in Sources */, E1559A76294D960C00C1FFBC /* MainOverlay.swift in Sources */, E14EDECC2B8FB709000F00A4 /* ItemYear.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 3a015b51d..248caf57d 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -25,8 +25,11 @@ struct AddItemImageView: View { @EnvironmentObject private var router: ItemEditorCoordinator.Router + @ObservedObject + private var viewModel: ItemImagesViewModel + @StateObject - var viewModel: RemoteImageInfoViewModel + private var remoteImageInfoViewModel: RemoteImageInfoViewModel // MARK: - Dialog States @@ -43,11 +46,21 @@ struct AddItemImageView: View { @State private var layout: CollectionVGridLayout = .minWidth(150) + // MARK: - Initializer + + init(viewModel: ItemImagesViewModel) { + self._viewModel = ObservedObject(wrappedValue: viewModel) + self._remoteImageInfoViewModel = StateObject(wrappedValue: RemoteImageInfoViewModel( + item: viewModel.item, + imageType: viewModel.selectedType! + )) + } + // MARK: - Body var body: some View { contentView - .navigationBarTitle(viewModel.imageType.rawValue.localizedCapitalized) + .navigationBarTitle(remoteImageInfoViewModel.imageType.rawValue.localizedCapitalized) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(viewModel.state == .updating) .topBarTrailing { @@ -56,10 +69,12 @@ struct AddItemImageView: View { } } .onFirstAppear { - viewModel.send(.refresh) + remoteImageInfoViewModel.send(.refresh) } .onReceive(viewModel.events) { event in switch event { + case .deleted: + break case .updated: UIDevice.feedback(.success) router.pop() @@ -80,13 +95,11 @@ struct AddItemImageView: View { @ViewBuilder private var contentView: some View { - switch viewModel.state { - case .initial: + switch remoteImageInfoViewModel.state { + case .initial, .refreshing: DelayedProgressView() case .content: gridView - case .updating: - updateView case let .error(error): ErrorView(error: error) .onRetry { @@ -106,13 +119,13 @@ struct AddItemImageView: View { .listRowInsets(.zero) } else { CollectionVGrid( - uniqueElements: viewModel.images, + uniqueElements: remoteImageInfoViewModel.elements, layout: layout ) { image in imageButton(image) } .onReachedBottomEdge(offset: .offset(300)) { - viewModel.send(.getNextPage) + remoteImageInfoViewModel.send(.getNextPage) } } } @@ -178,21 +191,21 @@ struct AddItemImageView: View { // MARK: - Set Image Confirmation @ViewBuilder - private func confirmationSheet(_ image: RemoteImageInfo) -> some View { + private func confirmationSheet(_ remoteImageInfo: RemoteImageInfo) -> some View { NavigationView { VStack { posterImage( - image, - posterStyle: image.height ?? 0 > image.width ?? 0 ? .portrait : .landscape + remoteImageInfo, + posterStyle: remoteImageInfo.height ?? 0 > remoteImageInfo.width ?? 0 ? .portrait : .landscape ) .scaledToFit() - if let imageWidth = image.width, let imageHeight = image.height { + if let imageWidth = remoteImageInfo.width, let imageHeight = remoteImageInfo.height { Text("\(imageWidth) x \(imageHeight)") .font(.body) } - Text(image.providerName ?? .emptyDash) + Text(remoteImageInfo.providerName ?? .emptyDash) .font(.caption) .foregroundStyle(.secondary) } @@ -204,10 +217,7 @@ struct AddItemImageView: View { } .topBarTrailing { Button(L10n.save) { - if let newURL = image.url { - viewModel.send(.setImage(url: newURL, type: viewModel.imageType)) - } - selectedImage = nil + viewModel.send(.setImage(remoteImageInfo)) } .buttonStyle(.toolbarPill) } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift index 7ba23f3c2..2c11dc273 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift @@ -32,8 +32,6 @@ struct EditItemImagesView: View { @State private var selectedImage: ImageInfo? - @State - private var uploadType: ImageType? // MARK: - Error State @@ -63,19 +61,14 @@ struct EditItemImagesView: View { deletionSheet(imageInfo) } .fileImporter( - isPresented: .constant(uploadType != nil), + isPresented: .constant(viewModel.selectedType != nil), allowedContentTypes: [.image], allowsMultipleSelection: false ) { switch $0 { case let .success(urls): if let url = urls.first { - do { - if let uploadType { - viewModel.send(.uploadImage(url: url, type: uploadType)) - } - uploadType = nil - } + viewModel.send(.uploadImage(url)) } case let .failure(fileError): self.error = fileError @@ -143,12 +136,10 @@ struct EditItemImagesView: View { Spacer() Menu(L10n.options, systemImage: "plus") { Button(action: { + viewModel.send(.selectType(imageType)) router.route( to: \.addImage, - RemoteImageInfoViewModel( - item: viewModel.item, - imageType: imageType - ) + viewModel ) }) { Label(L10n.search, systemImage: "magnifyingglass") @@ -157,13 +148,13 @@ struct EditItemImagesView: View { Divider() Button(action: { - uploadType = imageType + viewModel.send(.selectType(imageType)) }) { Label(L10n.uploadFile, systemImage: "document.badge.plus") } Button(action: { - uploadType = imageType + viewModel.send(.selectType(imageType)) }) { Label(L10n.uploadPhoto, systemImage: "photo.badge.plus") } From f3a53437b1cd22765545a636868d243c2a291358 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 4 Jan 2025 22:50:13 -0700 Subject: [PATCH 19/45] Close! --- .../Coordinators/ItemEditorCoordinator.swift | 17 +- .../Coordinators/ItemImagesCoordinator.swift | 63 +++++++ .../ItemImagesViewModel.swift | 166 +++++++++++------- Swiftfin.xcodeproj/project.pbxproj | 48 ++++- Swiftfin/Components/ListTitleSection.swift | 1 + .../Views/ItemEditorView/ItemEditorView.swift | 2 +- .../AddItemImageView/AddItemImageView.swift | 70 ++------ .../Components/LocalImageInfoView.swift | 95 ++++++++++ .../Components/RemoteImageInfoView.swift | 96 ++++++++++ .../ItemImageDetailsView.swift | 105 +++++++++++ .../ItemImagesView.swift} | 58 ++---- 11 files changed, 532 insertions(+), 189 deletions(-) create mode 100644 Shared/Coordinators/ItemImagesCoordinator.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift rename Swiftfin/Views/ItemEditorView/ItemImages/{EditItemImagesView/EditItemImagesView.swift => ItemImagesView/ItemImagesView.swift} (72%) diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 166032301..e649148d6 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -26,12 +26,10 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @Route(.modal) var editMetadata = makeEditMetadata - // MARK: - Route to Metadata + // MARK: - Route to Images - @Route(.push) - var editImages = makeEditImages - @Route(.push) - var addImage = makeAddImage + @Route(.modal) + var imageEditor = makeImageEditor // MARK: - Route to Genres @@ -82,13 +80,8 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Item Images - @ViewBuilder - func makeEditImages(item: BaseItemDto) -> some View { - EditItemImagesView(viewModel: ItemImagesViewModel(item: item)) - } - - func makeAddImage(viewModel: ItemImagesViewModel) -> some View { - AddItemImageView(viewModel: viewModel) + func makeImageEditor(viewModel: ItemImagesViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemImagesCoordinator(viewModel: viewModel)) } // MARK: - Item Genres diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift new file mode 100644 index 000000000..1bcf4515c --- /dev/null +++ b/Shared/Coordinators/ItemImagesCoordinator.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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import Stinsen +import SwiftUI + +final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { + + let stack = NavigationStack(initial: \ItemImagesCoordinator.start) + + @Root + var start = makeStart + + @ObservedObject + private var viewModel: ItemImagesViewModel + + // MARK: - Route to Views + + @Route(.push) + var addImage = makeAddImage + @Route(.modal) + var deleteImage = makeDeleteImage + @Route(.modal) + var selectImage = makeSelectImage + + // MARK: - Initializer + + init(viewModel: ItemImagesViewModel) { + self._viewModel = ObservedObject(wrappedValue: viewModel) + } + + // MARK: - Item Images + + @ViewBuilder + func makeAddImage(imageType: ImageType) -> some View { + AddItemImageView(viewModel: viewModel, imageType: imageType) + } + + func makeDeleteImage(imageInfo: (key: ImageInfo, value: UIImage)) -> NavigationViewCoordinator { + NavigationViewCoordinator { + ItemImageDetailsView(viewModel: self.viewModel, localImageInfo: imageInfo) + } + } + + func makeSelectImage(remoteImageInfo: RemoteImageInfo) -> NavigationViewCoordinator { + NavigationViewCoordinator { + ItemImageDetailsView(viewModel: self.viewModel, remoteImageInfo: remoteImageInfo) + } + } + + // MARK: - Start + + @ViewBuilder + func makeStart() -> some View { + ItemImagesView(viewModel: self.viewModel) + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 48d3efe37..06999f891 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -12,7 +12,6 @@ import IdentifiedCollections import JellyfinAPI import OrderedCollections import SwiftUI -import UniformTypeIdentifiers class ItemImagesViewModel: ViewModel, Stateful, Eventful { @@ -26,9 +25,9 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { case cancel case refresh case backgroundRefresh - case selectType(ImageType?) case setImage(RemoteImageInfo) - case uploadImage(URL) + case uploadPhoto(image: UIImage, type: ImageType) + case uploadImage(file: URL, type: ImageType) case deleteImage(ImageInfo) } @@ -53,8 +52,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { @Published var item: BaseItemDto @Published - var selectedType: ImageType? - @Published var images: [ImageInfo: UIImage] = [:] // MARK: - State Management @@ -95,13 +92,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .selectType(type): - task?.cancel() - - self.selectedType = type - - return state - case .backgroundRefresh: task?.cancel() @@ -185,7 +175,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case let .uploadImage(url): + case let .uploadPhoto(image, type): task?.cancel() task = Task { [weak self] in @@ -195,7 +185,35 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { self.state = .updating } - try await self.uploadImage(url) + try await self.uploadPhoto(image, type: type) + try await self.refreshItem() + + await MainActor.run { + self.state = .content + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + + return state + + case let .uploadImage(url, type): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + self.state = .updating + } + + try await self.uploadImage(url, type: type) try await self.refreshItem() await MainActor.run { @@ -306,79 +324,94 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: imageURL) let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) try await userSession.client.send(imageRequest) - - await MainActor.run { - self.selectedType = nil - } } - // MARK: - Upload Image - - private func uploadImage(_ url: URL) async throws { - guard let itemID = item.id, - let type = selectedType else { return } + // MARK: - Upload Image/File - guard url.startAccessingSecurityScopedResource() else { - throw JellyfinAPIError("Unable to access file at \(url)") - } - defer { url.stopAccessingSecurityScopedResource() } + private func upload(imageData: Data, imageType: ImageType, contentType: String) async throws { + guard let itemID = item.id else { return } - let fileData = try Data(contentsOf: url) - let fileSize = fileData.count + let uploadLimit: Int = 30_000_000 - guard fileSize < 30_000_000 else { + guard imageData.count <= uploadLimit else { throw JellyfinAPIError( - "This image is too large (\(fileSize.formatted(.byteCount(style: .file)))). The upload limit for images is 30 MB." + "This image (\(imageData.count.formatted(.byteCount(style: .file)))) exceeds the maximum allowed size for upload (\(uploadLimit.formatted(.byteCount(style: .file)))." ) } + var request = Paths.setItemImage( + itemID: itemID, + imageType: imageType.rawValue, + imageData.base64EncodedData() + ) + request.headers = ["Content-Type": contentType] + + _ = try await userSession.client.send(request) + } + + // MARK: - Prepare Photo for Upload + + private func uploadPhoto(_ image: UIImage, type: ImageType) async throws { let contentType: String let imageData: Data - if let fileType = UTType(filenameExtension: url.pathExtension) { - if fileType.conforms(to: .png) { - contentType = "image/png" - imageData = fileData - } else if fileType.conforms(to: .jpeg) { - contentType = "image/jpeg" - imageData = fileData - } else { - guard let image = UIImage(data: fileData) else { - throw JellyfinAPIError("Unable to load image from file") - } - - if let pngData = image.pngData() { - contentType = "image/png" - imageData = pngData - } else if let jpgData = image.jpegData(compressionQuality: 1) { - contentType = "image/jpeg" - imageData = jpgData - } else { - throw JellyfinAPIError("Failed to convert image to png/jpg") - } - } + if let pngData = image.pngData() { + contentType = "image/png" + imageData = pngData + } else if let jpgData = image.jpegData(compressionQuality: 1) { + contentType = "image/jpeg" + imageData = jpgData } else { - throw JellyfinAPIError("Unsupported file type") + logger.error("Unable to convert given profile image to png/jpg") + throw JellyfinAPIError("An internal error occurred") } - var request = Paths.setItemImage( - itemID: itemID, - imageType: type.rawValue, - imageData.base64EncodedData() + try await upload( + imageData: imageData, + imageType: type, + contentType: contentType ) - request.headers = ["Content-Type": contentType] + } - guard request.bodySize < 30_000_000 else { - throw JellyfinAPIError( - "Converting this image into a valid format has resulted in a larger image (\(request.bodySize.formatted(.byteCount(style: .file)))) that is now too large for upload. Before conversion this image was \(fileData.count.formatted(.byteCount(style: .file))). The upload limit for images is 30 MB." - ) + // MARK: - Prepare Image for Upload + + private func uploadImage(_ url: URL, type: ImageType) async throws { + guard url.startAccessingSecurityScopedResource() else { + throw JellyfinAPIError("Unable to access file at \(url)") } + defer { url.stopAccessingSecurityScopedResource() } - _ = try await userSession.client.send(request) + let contentType: String + let imageData: Data - await MainActor.run { - self.selectedType = nil + switch url.pathExtension.lowercased() { + case "png": + contentType = "image/png" + imageData = try Data(contentsOf: url) + case "jpeg", "jpg": + contentType = "image/jpeg" + imageData = try Data(contentsOf: url) + default: + guard let image = try UIImage(data: Data(contentsOf: url)) else { + throw JellyfinAPIError("Unable to load image from file") + } + + if let pngData = image.pngData() { + contentType = "image/png" + imageData = pngData + } else if let jpgData = image.jpegData(compressionQuality: 1) { + contentType = "image/jpeg" + imageData = jpgData + } else { + throw JellyfinAPIError("Failed to convert image to png/jpg") + } } + + try await upload( + imageData: imageData, + imageType: type, + contentType: contentType + ) } // MARK: - Delete Image @@ -428,7 +461,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { result[updatedInfo] = pair.value } - self.selectedType = nil self.images = updatedImages } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 5bc00cfde..bb9b7bfbd 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -77,7 +77,7 @@ 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; 4E45939F2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; - 4E4593A32D04E2B500E277E1 /* EditItemImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */; }; + 4E4593A32D04E2B500E277E1 /* ItemImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */; }; 4E4593A62D04E4E300E277E1 /* AddItemImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */; }; 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */; }; @@ -186,6 +186,11 @@ 4EA397472CD31CC000904C25 /* AddServerUserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */; }; 4EA78B0F2D29B0880093BFCE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B0E2D29B0820093BFCE /* Request.swift */; }; 4EA78B102D29B0880093BFCE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B0E2D29B0820093BFCE /* Request.swift */; }; + 4EA78B122D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */; }; + 4EA78B132D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */; }; + 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */; }; + 4EA78B182D2A265E0093BFCE /* RemoteImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */; }; + 4EA78B1A2D2A26670093BFCE /* LocalImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; 4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; }; 4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; }; @@ -1252,7 +1257,7 @@ 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsSection.swift; sourceTree = ""; }; 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInput.swift; sourceTree = ""; }; 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesViewModel.swift; sourceTree = ""; }; - 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemImagesView.swift; sourceTree = ""; }; + 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesView.swift; sourceTree = ""; }; 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemImageView.swift; sourceTree = ""; }; 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = ""; }; 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionSection.swift; sourceTree = ""; }; @@ -1335,6 +1340,10 @@ 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysRow.swift; sourceTree = ""; }; 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserViewModel.swift; sourceTree = ""; }; 4EA78B0E2D29B0820093BFCE /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesCoordinator.swift; sourceTree = ""; }; + 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsView.swift; sourceTree = ""; }; + 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfoView.swift; sourceTree = ""; }; + 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImageInfoView.swift; sourceTree = ""; }; 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = ""; }; 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = ""; }; 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = ""; }; @@ -2292,7 +2301,8 @@ isa = PBXGroup; children = ( 4E4593A42D04E4D600E277E1 /* AddItemImageView */, - 4E4593A12D04E2A200E277E1 /* EditItemImagesView */, + 4EA78B152D2A0C4A0093BFCE /* ItemImageDetailsView */, + 4E4593A12D04E2A200E277E1 /* ItemImagesView */, ); path = ItemImages; sourceTree = ""; @@ -2316,12 +2326,12 @@ path = PlaybackBitrate; sourceTree = ""; }; - 4E4593A12D04E2A200E277E1 /* EditItemImagesView */ = { + 4E4593A12D04E2A200E277E1 /* ItemImagesView */ = { isa = PBXGroup; children = ( - 4E4593A22D04E2AF00E277E1 /* EditItemImagesView.swift */, + 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */, ); - path = EditItemImagesView; + path = ItemImagesView; sourceTree = ""; }; 4E4593A42D04E4D600E277E1 /* AddItemImageView */ = { @@ -2688,6 +2698,24 @@ path = Components; sourceTree = ""; }; + 4EA78B152D2A0C4A0093BFCE /* ItemImageDetailsView */ = { + isa = PBXGroup; + children = ( + 4EA78B1B2D2A266A0093BFCE /* Components */, + 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */, + ); + path = ItemImageDetailsView; + sourceTree = ""; + }; + 4EA78B1B2D2A266A0093BFCE /* Components */ = { + isa = PBXGroup; + children = ( + 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */, + 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */, + ); + path = Components; + sourceTree = ""; + }; 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */ = { isa = PBXGroup; children = ( @@ -3490,6 +3518,7 @@ 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */, + 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, E102312B2BCF8A08009D71FC /* LiveTVCoordinator */, C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */, @@ -5723,6 +5752,7 @@ E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */, E1CB757D2C80F00D00217C76 /* TranscodingProfile.swift in Sources */, E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */, + 4EA78B122D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */, E18ACA8D2A14773500BB4F35 /* (null) in Sources */, E10B1E8E2BD7708900A92EAF /* QuickConnectView.swift in Sources */, ); @@ -5771,6 +5801,7 @@ 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */, + 4EA78B182D2A265E0093BFCE /* RemoteImageInfoView.swift in Sources */, E1F5CF052CB09EA000607465 /* CurrentDate.swift in Sources */, E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */, 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */, @@ -5789,7 +5820,7 @@ E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */, E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */, E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */, - 4E4593A32D04E2B500E277E1 /* EditItemImagesView.swift in Sources */, + 4E4593A32D04E2B500E277E1 /* ItemImagesView.swift in Sources */, 4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */, @@ -5960,6 +5991,7 @@ 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, E10E67B72CF515130095365B /* Binding.swift in Sources */, E119696A2CC99EA9001A58BE /* ServerTaskProgressSection.swift in Sources */, + 4EA78B132D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */, E1BAFE102BE921270069C4D7 /* SwiftfinApp+ValueObservation.swift in Sources */, E1ED7FDE2CAA641F00ACB6E3 /* ListTitleSection.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, @@ -6030,6 +6062,7 @@ E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */, 4E01446D2D0292E200193038 /* Trie.swift in Sources */, E1937A61288F32DB00CB80AA /* Poster.swift in Sources */, + 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */, 4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */, E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift in Sources */, E1CB757C2C80F00D00217C76 /* TranscodingProfile.swift in Sources */, @@ -6121,6 +6154,7 @@ 4E35CE612CBED3F300DBD886 /* TimeLimitSection.swift in Sources */, E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */, + 4EA78B1A2D2A26670093BFCE /* LocalImageInfoView.swift in Sources */, E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */, 4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */, E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */, diff --git a/Swiftfin/Components/ListTitleSection.swift b/Swiftfin/Components/ListTitleSection.swift index b330dfff5..c9d22a467 100644 --- a/Swiftfin/Components/ListTitleSection.swift +++ b/Swiftfin/Components/ListTitleSection.swift @@ -25,6 +25,7 @@ struct ListTitleSection: View { Text(title) .font(.title3) .fontWeight(.semibold) + .multilineTextAlignment(.center) if let description { Text(description) diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index cb53af0fc..f72b8e9c8 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -106,7 +106,7 @@ struct ItemEditorView: View { } ChevronButton(L10n.images) .onSelect { - router.route(to: \.editImages, viewModel.item) + router.route(to: \.imageEditor, ItemImagesViewModel(item: viewModel.item)) } ChevronButton(L10n.metadata) .onSelect { diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 248caf57d..a2069c3c4 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -20,15 +20,15 @@ struct AddItemImageView: View { @Default(.accentColor) private var accentColor - // MARK: - State, Observed, & Environment Objects + // MARK: - Observed, & Environment Objects @EnvironmentObject - private var router: ItemEditorCoordinator.Router + private var router: ItemImagesCoordinator.Router @ObservedObject private var viewModel: ItemImagesViewModel - @StateObject + @ObservedObject private var remoteImageInfoViewModel: RemoteImageInfoViewModel // MARK: - Dialog States @@ -36,11 +36,6 @@ struct AddItemImageView: View { @State private var error: Error? - // MARK: - Selected Image - - @State - private var selectedImage: RemoteImageInfo? - // MARK: - Collection Layout @State @@ -48,12 +43,14 @@ struct AddItemImageView: View { // MARK: - Initializer - init(viewModel: ItemImagesViewModel) { + init(viewModel: ItemImagesViewModel, imageType: ImageType) { self._viewModel = ObservedObject(wrappedValue: viewModel) - self._remoteImageInfoViewModel = StateObject(wrappedValue: RemoteImageInfoViewModel( - item: viewModel.item, - imageType: viewModel.selectedType! - )) + self._remoteImageInfoViewModel = ObservedObject( + initialValue: RemoteImageInfoViewModel( + item: viewModel.item, + imageType: imageType + ) + ) } // MARK: - Body @@ -68,7 +65,7 @@ struct AddItemImageView: View { ProgressView() } } - .onFirstAppear { + .onAppear { remoteImageInfoViewModel.send(.refresh) } .onReceive(viewModel.events) { event in @@ -84,11 +81,6 @@ struct AddItemImageView: View { } } .errorMessage($error) - .sheet(item: $selectedImage, onDismiss: { - selectedImage = nil - }) { selectedImage in - confirmationSheet(selectedImage) - } } // MARK: - Content View @@ -148,7 +140,9 @@ struct AddItemImageView: View { private func imageButton(_ image: RemoteImageInfo?) -> some View { Button { - selectedImage = image + if let image { + router.route(to: \.selectImage, image) + } } label: { posterImage( image, @@ -187,40 +181,4 @@ struct AddItemImageView: View { } .posterStyle(posterStyle) } - - // MARK: - Set Image Confirmation - - @ViewBuilder - private func confirmationSheet(_ remoteImageInfo: RemoteImageInfo) -> some View { - NavigationView { - VStack { - posterImage( - remoteImageInfo, - posterStyle: remoteImageInfo.height ?? 0 > remoteImageInfo.width ?? 0 ? .portrait : .landscape - ) - .scaledToFit() - - if let imageWidth = remoteImageInfo.width, let imageHeight = remoteImageInfo.height { - Text("\(imageWidth) x \(imageHeight)") - .font(.body) - } - - Text(remoteImageInfo.providerName ?? .emptyDash) - .font(.caption) - .foregroundStyle(.secondary) - } - .padding(.horizontal) - .navigationTitle(L10n.replaceImages) - .navigationBarTitleDisplayMode(.inline) - .navigationBarCloseButton { - selectedImage = nil - } - .topBarTrailing { - Button(L10n.save) { - viewModel.send(.setImage(remoteImageInfo)) - } - .buttonStyle(.toolbarPill) - } - } - } } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift new file mode 100644 index 000000000..8296eaf72 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift @@ -0,0 +1,95 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + struct LocalImageInfoView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - Image Info + + let imageInfo: ImageInfo + let image: UIImage + + // MARK: - Image Functions + + let onDelete: () -> Void + + // MARK: - Header + + @ViewBuilder + private var header: some View { + Section { + Image(uiImage: image) + .resizable() + .scaledToFit() + } + .listRowBackground(Color.clear) + .listRowCornerRadius(0) + .listRowInsets(.zero) + } + + // MARK: - Details + + @ViewBuilder + private var details: some View { + Section(L10n.details) { + + TextPairView(leading: L10n.id, trailing: imageInfo.id.description) + + if let index = imageInfo.imageIndex { + TextPairView(leading: L10n.type, trailing: index.description) + } + + if let imageTag = imageInfo.imageTag { + TextPairView(leading: L10n.tag, trailing: imageTag) + } + + if let imageType = imageInfo.imageType { + TextPairView(leading: L10n.type, trailing: imageType.rawValue) + } + + if let width = imageInfo.width, let height = imageInfo.height { + TextPairView(leading: "Dimensions", trailing: "\(width) x \(height)") + } + + if let path = imageInfo.path { + TextPairView(leading: "Path", trailing: path) + .onSubmit { + UIApplication.shared.open(URL(string: path)!) + } + } + } + } + + var deleteButton: some View { + ListRowButton(L10n.delete) { + onDelete() + } + .foregroundStyle( + accentColor.overlayColor, + .red + ) + } + + var body: some View { + List { + header + details + deleteButton + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift new file mode 100644 index 000000000..322db3ecf --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift @@ -0,0 +1,96 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + struct RemoteImageInfoView: View { + + // MARK: - Image Info + + let imageInfo: RemoteImageInfo + + // MARK: - Image Functions + + let onSave: () -> Void + + // MARK: - Header + + @ViewBuilder + private var header: some View { + Section { + ImageView(URL(string: imageInfo.url ?? nil)) + .placeholder { _ in + Image(systemName: imageInfo.systemImage) + } + .failure { + Image(systemName: imageInfo.systemImage) + } + .scaledToFit() + .frame(maxWidth: .infinity) + .accessibilityIgnoresInvertColors() + } + .listRowBackground(Color.clear) + .listRowCornerRadius(0) + .listRowInsets(.zero) + } + + // MARK: - Details + + @ViewBuilder + private var details: some View { + Section(L10n.details) { + + if let providerName = imageInfo.providerName { + TextPairView(leading: L10n.provider, trailing: providerName) + } + + TextPairView(leading: L10n.id, trailing: imageInfo.id.description) + + if let communityRating = imageInfo.communityRating { + TextPairView(leading: L10n.rating, trailing: communityRating.description) + } + + if let voteCount = imageInfo.voteCount { + TextPairView(leading: "Votes", trailing: voteCount.description) + } + + if let language = imageInfo.language { + TextPairView(leading: L10n.language, trailing: language) + } + + if let imageType = imageInfo.type { + TextPairView(leading: L10n.type, trailing: imageType.rawValue) + } + + if let width = imageInfo.width, let height = imageInfo.height { + TextPairView(leading: "Dimensions", trailing: "\(width) x \(height)") + } + + if let url = imageInfo.url { + TextPairView(leading: L10n.url, trailing: url) + .onSubmit { + UIApplication.shared.open(URL(string: url)!) + } + } + } + } + + var body: some View { + List { + header + details + } + .topBarTrailing { + Button(L10n.save, action: onSave) + .buttonStyle(.toolbarPill) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift new file mode 100644 index 000000000..7a3edaa37 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -0,0 +1,105 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import BlurHashKit +import CollectionVGrid +import Combine +import Defaults +import JellyfinAPI +import Nuke +import SwiftUI + +struct ItemImageDetailsView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + var viewModel: ItemImagesViewModel + + // MARK: - Image Variable + + private let localImageInfo: (key: ImageInfo, value: UIImage)? + + private let remoteImageInfo: RemoteImageInfo? + + // MARK: - Dialog States + + @State + private var error: Error? + + // MARK: - Collection Layout + + @State + private var layout: CollectionVGridLayout = .minWidth(150) + + // MARK: - Initializer + + init(viewModel: ItemImagesViewModel, localImageInfo: (key: ImageInfo, value: UIImage)? = nil, remoteImageInfo: RemoteImageInfo? = nil) { + self._viewModel = ObservedObject(wrappedValue: viewModel) + self.localImageInfo = localImageInfo + self.remoteImageInfo = remoteImageInfo + } + + // MARK: - Body + + var body: some View { + contentView + .navigationBarTitle( + localImageInfo?.key.imageType?.rawValue.localizedCapitalized ?? + remoteImageInfo?.type?.rawValue.localizedCapitalized ?? + "" + ) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + router.dismissCoordinator() + } + .topBarTrailing { + if viewModel.backgroundStates.contains(.refreshing) { + ProgressView() + } + } + .onReceive(viewModel.events) { event in + switch event { + case .deleted: + UIDevice.feedback(.success) + router.dismissCoordinator() + case .updated: + break + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + } + } + .errorMessage($error) + } + + // MARK: - Content View + + @ViewBuilder + var contentView: some View { + if let imageInfo = localImageInfo { + LocalImageInfoView(imageInfo: imageInfo.key, image: imageInfo.value) { + viewModel.send(.deleteImage(imageInfo.key)) + } + } else if let imageInfo = remoteImageInfo { + RemoteImageInfoView(imageInfo: imageInfo) { + viewModel.send(.setImage(imageInfo)) + } + } else { + ErrorView(error: JellyfinAPIError("No image provided.")) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift similarity index 72% rename from Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift rename to Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 2c11dc273..5be4c6c2e 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/EditItemImagesView/EditItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -13,7 +13,7 @@ import Defaults import JellyfinAPI import SwiftUI -struct EditItemImagesView: View { +struct ItemImagesView: View { // MARK: - Defaults @@ -23,7 +23,7 @@ struct EditItemImagesView: View { // MARK: - Observed & Environment Objects @EnvironmentObject - private var router: ItemEditorCoordinator.Router + private var router: ItemImagesCoordinator.Router @StateObject var viewModel: ItemImagesViewModel @@ -31,7 +31,7 @@ struct EditItemImagesView: View { // MARK: - Dialog State @State - private var selectedImage: ImageInfo? + private var selectedType: ImageType? // MARK: - Error State @@ -57,18 +57,18 @@ struct EditItemImagesView: View { .onFirstAppear { viewModel.send(.refresh) } - .sheet(item: $selectedImage) { imageInfo in - deletionSheet(imageInfo) + .navigationBarCloseButton { + router.dismissCoordinator() } .fileImporter( - isPresented: .constant(viewModel.selectedType != nil), + isPresented: .constant(selectedType != nil), allowedContentTypes: [.image], allowsMultipleSelection: false ) { switch $0 { case let .success(urls): - if let url = urls.first { - viewModel.send(.uploadImage(url)) + if let file = urls.first, let type = selectedType { + viewModel.send(.uploadImage(file: file, type: type)) } case let .failure(fileError): self.error = fileError @@ -118,7 +118,7 @@ struct EditItemImagesView: View { HStack { ForEach(sortedImageArray, id: \.key) { imageData in imageButton(imageData.value) { - selectedImage = imageData.key + router.route(to: \.deleteImage, imageData) } } } @@ -136,10 +136,9 @@ struct EditItemImagesView: View { Spacer() Menu(L10n.options, systemImage: "plus") { Button(action: { - viewModel.send(.selectType(imageType)) router.route( to: \.addImage, - viewModel + imageType ) }) { Label(L10n.search, systemImage: "magnifyingglass") @@ -148,13 +147,13 @@ struct EditItemImagesView: View { Divider() Button(action: { - viewModel.send(.selectType(imageType)) + selectedType = imageType }) { Label(L10n.uploadFile, systemImage: "document.badge.plus") } Button(action: { - viewModel.send(.selectType(imageType)) + selectedType = imageType }) { Label(L10n.uploadPhoto, systemImage: "photo.badge.plus") } @@ -184,37 +183,4 @@ struct EditItemImagesView: View { .padding(16) } } - - // MARK: - Delete Image Confirmation Sheet - - @ViewBuilder - private func deletionSheet(_ imageInfo: ImageInfo) -> some View { - if let image = viewModel.images[imageInfo] { - NavigationView { - VStack { - Image(uiImage: image) - .resizable() - .scaledToFit() - - Text("\(Int(image.size.width)) x \(Int(image.size.height))") - .font(.headline) - } - .padding(.horizontal) - .navigationTitle(L10n.deleteImage) - .navigationBarTitleDisplayMode(.inline) - .navigationBarCloseButton { - selectedImage = nil - } - .topBarTrailing { - Button(L10n.delete, role: .destructive) { - viewModel.send(.deleteImage(imageInfo)) - selectedImage = nil - } - .buttonStyle(.toolbarPill(.red)) - } - } - } else { - ErrorView(error: JellyfinAPIError(L10n.unknownError)) - } - } } From 22fb028854aebfb8b31a3b174da23ce9cac2800f Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 4 Jan 2025 22:56:14 -0700 Subject: [PATCH 20/45] First no all appears --- .../ItemImages/AddItemImageView/AddItemImageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index a2069c3c4..deab4c8d1 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -65,7 +65,7 @@ struct AddItemImageView: View { ProgressView() } } - .onAppear { + .onFirstAppear { remoteImageInfoViewModel.send(.refresh) } .onReceive(viewModel.events) { event in From ada6e01ad02a0a7c66d7cb3a234f9fb0ef2d2d21 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 4 Jan 2025 23:13:59 -0700 Subject: [PATCH 21/45] Fix double pop/routerdismiss --- .../AddItemImageView/AddItemImageView.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index deab4c8d1..35149599a 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -68,18 +68,6 @@ struct AddItemImageView: View { .onFirstAppear { remoteImageInfoViewModel.send(.refresh) } - .onReceive(viewModel.events) { event in - switch event { - case .deleted: - break - case .updated: - UIDevice.feedback(.success) - router.pop() - case let .error(eventError): - UIDevice.feedback(.error) - error = eventError - } - } .errorMessage($error) } From fd9734e41a8f1d47295dbc3a269aef16d37a66b3 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 5 Jan 2025 17:53:18 -0700 Subject: [PATCH 22/45] Uploading from Photos is (Finally) Ready! --- .../Coordinators/ItemImagesCoordinator.swift | 6 + .../Coordinators/ItemPhotoCoordinator.swift | 59 +++++ .../ItemImagesViewModel.swift | 2 + .../RemoteImageInfoViewModel.swift | 14 +- Swiftfin.xcodeproj/project.pbxproj | 34 ++- .../AddItemImageView/AddItemImageView.swift | 2 +- .../Components/LocalImageInfoView.swift | 20 +- .../Components/RemoteImageInfoView.swift | 5 +- .../ItemImageDetailsView.swift | 4 +- .../Components/ImageCropView.swift | 201 ++++++++++++++++++ .../ItemImagePicker/ItemImagePicker.swift | 28 +++ .../ItemImagesView/ItemImagesView.swift | 5 +- 12 files changed, 355 insertions(+), 25 deletions(-) create mode 100644 Shared/Coordinators/ItemPhotoCoordinator.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/ItemImagePicker.swift diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index 1bcf4515c..c378c102f 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -28,6 +28,8 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { var deleteImage = makeDeleteImage @Route(.modal) var selectImage = makeSelectImage + @Route(.modal) + var photoPicker = makePhotoPicker // MARK: - Initializer @@ -54,6 +56,10 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { } } + func makePhotoPicker(type: ImageType) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemPhotoCoordinator(viewModel: self.viewModel, type: type)) + } + // MARK: - Start @ViewBuilder diff --git a/Shared/Coordinators/ItemPhotoCoordinator.swift b/Shared/Coordinators/ItemPhotoCoordinator.swift new file mode 100644 index 000000000..9b96387b9 --- /dev/null +++ b/Shared/Coordinators/ItemPhotoCoordinator.swift @@ -0,0 +1,59 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import Stinsen +import SwiftUI + +final class ItemPhotoCoordinator: NavigationCoordinatable { + + // MARK: - Navigation Components + + let stack = Stinsen.NavigationStack(initial: \ItemPhotoCoordinator.start) + + @Root + var start = makeStart + + // MARK: - Routes + + @Route(.push) + var cropImage = makeCropImage + + // MARK: - Observed Object + + @ObservedObject + var viewModel: ItemImagesViewModel + + let type: ImageType + + // MARK: - Initializer + + init(viewModel: ItemImagesViewModel, type: ImageType) { + self.viewModel = viewModel + self.type = type + } + + // MARK: - Views + + func makeCropImage(image: UIImage) -> some View { + #if os(iOS) + ItemImagePicker.ImageCropView(viewModel: viewModel, image: image, type: type) + #else + AssertionFailureView("not implemented") + #endif + } + + @ViewBuilder + func makeStart() -> some View { + #if os(iOS) + ItemImagePicker() + #else + AssertionFailureView("not implemented") + #endif + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 06999f891..c2847239c 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -296,6 +296,8 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { ) let response = try await self.userSession.client.send(request) + // TODO: Is there a way for me to just get the Image URL so I can use + // ImageView instead of passing/storing the full images? if let image = UIImage(data: response.value) { return (imageInfo, image) } diff --git a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift index 6eef42471..a119b24f9 100644 --- a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift @@ -19,21 +19,22 @@ class RemoteImageInfoViewModel: PagingLibraryViewModel { @Published var item: BaseItemDto - @Published var imageType: ImageType - @Published - var includeAllLanguages: Bool + let providerName: String? + let includeAllLanguages: Bool init( item: BaseItemDto, imageType: ImageType, + providerName: String? = nil, includeAllLanguages: Bool = false, pageSize: Int = DefaultPageSize ) { self.item = item self.imageType = imageType + self.providerName = providerName self.includeAllLanguages = includeAllLanguages super.init(parent: nil, pageSize: pageSize) } @@ -42,13 +43,18 @@ class RemoteImageInfoViewModel: PagingLibraryViewModel { guard let itemID = item.id else { return [] } let startIndex = page * pageSize - let parameters = Paths.GetRemoteImagesParameters( + var parameters = Paths.GetRemoteImagesParameters( type: imageType, startIndex: startIndex, limit: pageSize, + providerName: providerName, isIncludeAllLanguages: includeAllLanguages ) + if let providerName = providerName { + parameters.providerName = providerName + } + let request = Paths.getRemoteImages(itemID: itemID, parameters: parameters) let response = try await userSession.client.send(request) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index bb9b7bfbd..2cf701f62 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -186,11 +186,14 @@ 4EA397472CD31CC000904C25 /* AddServerUserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */; }; 4EA78B0F2D29B0880093BFCE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B0E2D29B0820093BFCE /* Request.swift */; }; 4EA78B102D29B0880093BFCE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B0E2D29B0820093BFCE /* Request.swift */; }; - 4EA78B122D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */; }; 4EA78B132D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */; }; 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */; }; 4EA78B182D2A265E0093BFCE /* RemoteImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */; }; 4EA78B1A2D2A26670093BFCE /* LocalImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */; }; + 4EA78B202D2B5AA30093BFCE /* ItemImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemImagePicker.swift */; }; + 4EA78B232D2B5CFC0093BFCE /* ImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ImageCropView.swift */; }; + 4EA78B252D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; + 4EA78B262D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; 4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; }; 4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; }; @@ -1344,6 +1347,9 @@ 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsView.swift; sourceTree = ""; }; 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfoView.swift; sourceTree = ""; }; 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImageInfoView.swift; sourceTree = ""; }; + 4EA78B1F2D2B5A9E0093BFCE /* ItemImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePicker.swift; sourceTree = ""; }; + 4EA78B222D2B5CEF0093BFCE /* ImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCropView.swift; sourceTree = ""; }; + 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCoordinator.swift; sourceTree = ""; }; 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = ""; }; 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = ""; }; 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = ""; }; @@ -2300,6 +2306,7 @@ 4E37F6182D17EB220022AADD /* ItemImages */ = { isa = PBXGroup; children = ( + 4EA78B1E2D2B5A960093BFCE /* ItemImagePicker */, 4E4593A42D04E4D600E277E1 /* AddItemImageView */, 4EA78B152D2A0C4A0093BFCE /* ItemImageDetailsView */, 4E4593A12D04E2A200E277E1 /* ItemImagesView */, @@ -2716,6 +2723,23 @@ path = Components; sourceTree = ""; }; + 4EA78B1E2D2B5A960093BFCE /* ItemImagePicker */ = { + isa = PBXGroup; + children = ( + 4EA78B212D2B5CDD0093BFCE /* Components */, + 4EA78B1F2D2B5A9E0093BFCE /* ItemImagePicker.swift */, + ); + path = ItemImagePicker; + sourceTree = ""; + }; + 4EA78B212D2B5CDD0093BFCE /* Components */ = { + isa = PBXGroup; + children = ( + 4EA78B222D2B5CEF0093BFCE /* ImageCropView.swift */, + ); + path = Components; + sourceTree = ""; + }; 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */ = { isa = PBXGroup; children = ( @@ -3519,6 +3543,7 @@ 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */, 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */, + 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, E102312B2BCF8A08009D71FC /* LiveTVCoordinator */, C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */, @@ -4159,8 +4184,8 @@ children = ( E1763A282BF3046A004DF6AB /* AddUserButton.swift */, E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */, - E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */, BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */, + E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */, ); path = Components; sourceTree = ""; @@ -5715,6 +5740,7 @@ E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */, E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */, BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */, + 4EA78B262D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */, E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */, E1575E9A293E7B1E001665B1 /* Array.swift in Sources */, E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */, @@ -5752,7 +5778,6 @@ E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */, E1CB757D2C80F00D00217C76 /* TranscodingProfile.swift in Sources */, E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */, - 4EA78B122D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */, E18ACA8D2A14773500BB4F35 /* (null) in Sources */, E10B1E8E2BD7708900A92EAF /* QuickConnectView.swift in Sources */, ); @@ -6203,6 +6228,7 @@ E10B1ECD2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */, E1401CA92938140700E8B599 /* DarkAppIcon.swift in Sources */, E1A1529028FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */, + 4EA78B252D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */, E11042752B8013DF00821020 /* Stateful.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */, @@ -6210,6 +6236,7 @@ 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */, 4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */, E11C15352BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, + 4EA78B232D2B5CFC0093BFCE /* ImageCropView.swift in Sources */, E1D8428F2933F2D900D1041A /* MediaSourceInfo.swift in Sources */, E1BDF2EC2952290200CC0294 /* AspectFillActionButton.swift in Sources */, BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, @@ -6300,6 +6327,7 @@ E1F5CF092CB0A04500607465 /* Text.swift in Sources */, 4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */, E1B490442967E26300D3EDCE /* PersistentLogHandler.swift in Sources */, + 4EA78B202D2B5AA30093BFCE /* ItemImagePicker.swift in Sources */, E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */, E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */, E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 35149599a..c9b439cdd 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -92,7 +92,7 @@ struct AddItemImageView: View { @ViewBuilder private var gridView: some View { - if viewModel.images.isEmpty { + if remoteImageInfoViewModel.elements.isEmpty { Text(L10n.none) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .listRowSeparator(.hidden) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift index 8296eaf72..641c14507 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift @@ -32,9 +32,17 @@ extension ItemImageDetailsView { @ViewBuilder private var header: some View { Section { - Image(uiImage: image) - .resizable() - .scaledToFit() + ZStack { + Color.secondarySystemFill + + Image(uiImage: image) + .resizable() + .scaledToFill() + } + .scaledToFit() + .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) + .frame(maxWidth: .infinity) + .accessibilityIgnoresInvertColors() } .listRowBackground(Color.clear) .listRowCornerRadius(0) @@ -50,17 +58,13 @@ extension ItemImageDetailsView { TextPairView(leading: L10n.id, trailing: imageInfo.id.description) if let index = imageInfo.imageIndex { - TextPairView(leading: L10n.type, trailing: index.description) + TextPairView(leading: "Index", trailing: index.description) } if let imageTag = imageInfo.imageTag { TextPairView(leading: L10n.tag, trailing: imageTag) } - if let imageType = imageInfo.imageType { - TextPairView(leading: L10n.type, trailing: imageType.rawValue) - } - if let width = imageInfo.width, let height = imageInfo.height { TextPairView(leading: "Dimensions", trailing: "\(width) x \(height)") } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift index 322db3ecf..38ccf1ec2 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift @@ -34,6 +34,7 @@ extension ItemImageDetailsView { } .scaledToFit() .frame(maxWidth: .infinity) + .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) .accessibilityIgnoresInvertColors() } .listRowBackground(Color.clear) @@ -65,10 +66,6 @@ extension ItemImageDetailsView { TextPairView(leading: L10n.language, trailing: language) } - if let imageType = imageInfo.type { - TextPairView(leading: L10n.type, trailing: imageType.rawValue) - } - if let width = imageInfo.width, let height = imageInfo.height { TextPairView(leading: "Dimensions", trailing: "\(width) x \(height)") } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index 7a3edaa37..cce11f626 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -73,11 +73,9 @@ struct ItemImageDetailsView: View { } .onReceive(viewModel.events) { event in switch event { - case .deleted: + case .deleted, .updated: UIDevice.feedback(.success) router.dismissCoordinator() - case .updated: - break case let .error(eventError): UIDevice.feedback(.error) error = eventError diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift new file mode 100644 index 000000000..ad16a90bb --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift @@ -0,0 +1,201 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import Mantis +import SwiftUI + +extension ItemImagePicker { + + struct ImageCropView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: ItemPhotoCoordinator.Router + + @StateObject + private var proxy: _ImageCropView.Proxy = .init() + + @ObservedObject + var viewModel: ItemImagesViewModel + + // MARK: - Image Variable + + let image: UIImage + + let type: ImageType + + // MARK: - Error State + + @State + private var error: Error? = nil + + // MARK: - Body + + var body: some View { + _ImageCropView(initialImage: image, proxy: proxy) { + viewModel.send(.uploadPhoto(image: $0, type: type)) + } + .animation(.linear(duration: 0.1), value: viewModel.state) + .interactiveDismissDisabled(viewModel.state == .updating) + .navigationBarBackButtonHidden(viewModel.state == .updating) + .topBarTrailing { + + if viewModel.state == .initial { + Button(L10n.rotate, systemImage: "rotate.right") { + proxy.rotate() + } + .foregroundStyle(.gray) + } + + if viewModel.state == .updating { + Button(L10n.cancel) { + viewModel.send(.cancel) + } + .foregroundStyle(.red) + } else { + Button { + proxy.crop() + } label: { + Text(L10n.save) + .foregroundStyle(accentColor.overlayColor) + .font(.headline) + .padding(.vertical, 5) + .padding(.horizontal, 10) + .background { + accentColor + } + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + } + .toolbar { + ToolbarItem(placement: .principal) { + if viewModel.state == .updating { + ProgressView() + } else { + Button(L10n.reset) { + proxy.reset() + } + .foregroundStyle(.yellow) + .disabled(viewModel.state == .updating) + } + } + } + .ignoresSafeArea() + .background { + Color.black + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + error = eventError + case .deleted: + break + case .updated: + router.dismissCoordinator() + } + } + .errorMessage($error) + } + } + + // MARK: - Square Image Crop View + + struct _ImageCropView: UIViewControllerRepresentable { + + class Proxy: ObservableObject { + + weak var cropViewController: CropViewController? + + func crop() { + cropViewController?.crop() + } + + func reset() { + cropViewController?.didSelectReset() + } + + func rotate() { + cropViewController?.didSelectClockwiseRotate() + } + } + + let initialImage: UIImage + let proxy: Proxy + let onImageCropped: (UIImage) -> Void + + func makeUIViewController(context: Context) -> some UIViewController { + var config = Mantis.Config() + + config.cropViewConfig.backgroundColor = .black.withAlphaComponent(0.9) + config.cropViewConfig.cropShapeType = .rect + config.presetFixedRatioType = .canUseMultiplePresetFixedRatio() + config.showAttachedCropToolbar = false + + let cropViewController = Mantis.cropViewController( + image: initialImage, + config: config + ) + + cropViewController.delegate = context.coordinator + context.coordinator.onImageCropped = onImageCropped + + proxy.cropViewController = cropViewController + + return cropViewController + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + class Coordinator: CropViewControllerDelegate { + + var onImageCropped: ((UIImage) -> Void)? + + func cropViewControllerDidCrop( + _ cropViewController: CropViewController, + cropped: UIImage, + transformation: Transformation, + cropInfo: CropInfo + ) { + onImageCropped?(cropped) + } + + func cropViewControllerDidCancel( + _ cropViewController: CropViewController, + original: UIImage + ) {} + + func cropViewControllerDidFailToCrop( + _ cropViewController: CropViewController, + original: UIImage + ) {} + + func cropViewControllerDidBeginResize( + _ cropViewController: CropViewController + ) {} + + func cropViewControllerDidEndResize( + _ cropViewController: Mantis.CropViewController, + original: UIImage, + cropInfo: Mantis.CropInfo + ) {} + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/ItemImagePicker.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/ItemImagePicker.swift new file mode 100644 index 000000000..8e2320a78 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/ItemImagePicker.swift @@ -0,0 +1,28 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct ItemImagePicker: View { + + // MARK: - Observed, & Environment Objects + + @EnvironmentObject + private var router: ItemPhotoCoordinator.Router + + // MARK: - Body + + var body: some View { + UserProfileImagePicker.PhotoPicker { + router.dismissCoordinator() + } onSelectedImage: { image in + router.route(to: \.cropImage, image) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 5be4c6c2e..2ef858817 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -109,7 +109,7 @@ struct ItemImagesView: View { let filteredImages = viewModel.images.filter { $0.key.imageType == imageType } let imageArray = Array(filteredImages) - if !imageArray.isEmpty { + if imageArray.isNotEmpty { let sortedImageArray = imageArray.sorted { lhs, rhs in (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) } @@ -153,7 +153,7 @@ struct ItemImagesView: View { } Button(action: { - selectedType = imageType + router.route(to: \.photoPicker, imageType) }) { Label(L10n.uploadPhoto, systemImage: "photo.badge.plus") } @@ -177,6 +177,7 @@ struct ItemImagesView: View { .resizable() .scaledToFill() } + .scaledToFit() .posterStyle(image.size.height > image.size.width ? .portrait : .landscape) .frame(maxHeight: 150) .shadow(radius: 4) From 62bce8d400fe290b6d50d96a9faf196d2888a429 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Mon, 6 Jan 2025 13:37:03 -0700 Subject: [PATCH 23/45] wip --- Shared/Extensions/Hashable.swift | 16 ---------------- Shared/Extensions/JellyfinAPI/ImageInfo.swift | 6 +++--- Swiftfin.xcodeproj/project.pbxproj | 6 ------ .../ItemImagesView/ItemImagesView.swift | 14 +++++--------- 4 files changed, 8 insertions(+), 34 deletions(-) delete mode 100644 Shared/Extensions/Hashable.swift diff --git a/Shared/Extensions/Hashable.swift b/Shared/Extensions/Hashable.swift deleted file mode 100644 index 49bbb6a6d..000000000 --- a/Shared/Extensions/Hashable.swift +++ /dev/null @@ -1,16 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import Foundation - -extension Hashable { - - var hashString: String { - "\(hashValue)" - } -} diff --git a/Shared/Extensions/JellyfinAPI/ImageInfo.swift b/Shared/Extensions/JellyfinAPI/ImageInfo.swift index 8e2d3e958..e2374f2df 100644 --- a/Shared/Extensions/JellyfinAPI/ImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ImageInfo.swift @@ -9,9 +9,9 @@ import Foundation import JellyfinAPI -// TODO: How SHOULD I get Identifiable extension ImageInfo: @retroactive Identifiable { - public var id: String { - self.hashString + + public var id: Int { + hashValue } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2cf701f62..b494951de 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -826,8 +826,6 @@ E17FB55728C1256400311DFE /* CastAndCrewHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55628C1256400311DFE /* CastAndCrewHStack.swift */; }; E17FB55928C125E900311DFE /* StudiosHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55828C125E900311DFE /* StudiosHStack.swift */; }; E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55A28C1266400311DFE /* GenresHStack.swift */; }; - E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1803EA02BFBD6CF0039F90E /* Hashable.swift */; }; - E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1803EA02BFBD6CF0039F90E /* Hashable.swift */; }; E18121062CBE428000682985 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528728FD229500600579 /* ChevronButton.swift */; }; E18295E429CAC6F100F91ED0 /* BasicNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */; }; E18443CB2A037773002DDDC8 /* UDPBroadcast in Frameworks */ = {isa = PBXBuildFile; productRef = E18443CA2A037773002DDDC8 /* UDPBroadcast */; }; @@ -1780,7 +1778,6 @@ E17FB55628C1256400311DFE /* CastAndCrewHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewHStack.swift; sourceTree = ""; }; E17FB55828C125E900311DFE /* StudiosHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudiosHStack.swift; sourceTree = ""; }; E17FB55A28C1266400311DFE /* GenresHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenresHStack.swift; sourceTree = ""; }; - E1803EA02BFBD6CF0039F90E /* Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashable.swift; sourceTree = ""; }; E185920528CDAA6400326F80 /* CastAndCrewHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewHStack.swift; sourceTree = ""; }; E185920728CDAAA200326F80 /* SimilarItemsHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarItemsHStack.swift; sourceTree = ""; }; E185920928CEF23A00326F80 /* FocusGuide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusGuide.swift; sourceTree = ""; }; @@ -3489,7 +3486,6 @@ E133328729538D8D00EE76AB /* Files.swift */, E11CEB8C28999B4A003E74C7 /* Font.swift */, E10432F52BE4426F006FF9DD /* FormatStyle.swift */, - E1803EA02BFBD6CF0039F90E /* Hashable.swift */, E1E6C44A29AED2B70064123F /* HorizontalAlignment.swift */, E139CC1E28EC83E400688DE2 /* Int.swift */, E1AD105226D96D5F003E4A08 /* JellyfinAPI */, @@ -5495,7 +5491,6 @@ E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */, E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */, E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */, - E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */, 4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */, E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */, E1575E93293E7B1E001665B1 /* Double.swift in Sources */, @@ -5844,7 +5839,6 @@ 4EB7C8D52CCED6E7000CC011 /* AddServerUserView.swift in Sources */, E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */, E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */, - E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */, 4E4593A32D04E2B500E277E1 /* ItemImagesView.swift in Sources */, 4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 2ef858817..8d2e88efb 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -133,29 +133,25 @@ struct ItemImagesView: View { HStack(alignment: .center, spacing: 16) { Text(imageType.rawValue.localizedCapitalized) .font(.headline) + Spacer() + Menu(L10n.options, systemImage: "plus") { - Button(action: { + Button(L10n.search, systemImage: "magnifyingglass") { router.route( to: \.addImage, imageType ) - }) { - Label(L10n.search, systemImage: "magnifyingglass") } Divider() - Button(action: { + Button(L10n.uploadFile, systemImage: "document.badge.plus") { selectedType = imageType - }) { - Label(L10n.uploadFile, systemImage: "document.badge.plus") } - Button(action: { + Button(L10n.uploadPhoto, systemImage: "photo.badge.plus") { router.route(to: \.photoPicker, imageType) - }) { - Label(L10n.uploadPhoto, systemImage: "photo.badge.plus") } } .font(.body) From 751b977dd8fa797f5d922df469adbac505e0c554 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 15:26:39 -0700 Subject: [PATCH 24/45] Reuse PhotoPicker and Crop code. --- .../Coordinators/ItemPhotoCoordinator.swift | 2 +- .../UserProfileImageCoordinator.swift | 4 +- .../UserProfileImageViewModel.swift | 2 +- Swiftfin.xcodeproj/project.pbxproj | 66 ++++-- .../Components/ImageCropView.swift | 201 ------------------ .../Components/ItemPhotoCropView.swift | 69 ++++++ .../ItemPhotoPickerView.swift} | 6 +- .../Components/PhotoCropView.swift | 188 ++++++++++++++++ .../PhotoPickerView/PhotoPickerView.swift | 87 ++++++++ .../Components/PhotoPicker.swift | 90 -------- .../Components/SquareImageCropView.swift | 198 ----------------- .../Components/UserProfileImageCropView.swift | 67 ++++++ ...swift => UserProfileImagePickerView.swift} | 8 +- 13 files changed, 465 insertions(+), 523 deletions(-) delete mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift rename Swiftfin/Views/ItemEditorView/ItemImages/{ItemImagePicker/ItemImagePicker.swift => ItemPhotoPickerView/ItemPhotoPickerView.swift} (80%) create mode 100644 Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift create mode 100644 Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift delete mode 100644 Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift delete mode 100644 Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift create mode 100644 Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift rename Swiftfin/Views/UserProfileImagePicker/{UserProfileImagePicker.swift => UserProfileImagePickerView.swift} (79%) diff --git a/Shared/Coordinators/ItemPhotoCoordinator.swift b/Shared/Coordinators/ItemPhotoCoordinator.swift index 9b96387b9..96d925c31 100644 --- a/Shared/Coordinators/ItemPhotoCoordinator.swift +++ b/Shared/Coordinators/ItemPhotoCoordinator.swift @@ -42,7 +42,7 @@ final class ItemPhotoCoordinator: NavigationCoordinatable { func makeCropImage(image: UIImage) -> some View { #if os(iOS) - ItemImagePicker.ImageCropView(viewModel: viewModel, image: image, type: type) + ItemPhotoCropView(viewModel: viewModel, image: image, type: type) #else AssertionFailureView("not implemented") #endif diff --git a/Shared/Coordinators/UserProfileImageCoordinator.swift b/Shared/Coordinators/UserProfileImageCoordinator.swift index d9b217839..393db01cf 100644 --- a/Shared/Coordinators/UserProfileImageCoordinator.swift +++ b/Shared/Coordinators/UserProfileImageCoordinator.swift @@ -38,7 +38,7 @@ final class UserProfileImageCoordinator: NavigationCoordinatable { func makeCropImage(image: UIImage) -> some View { #if os(iOS) - UserProfileImagePicker.SquareImageCropView(viewModel: viewModel, image: image) + UserProfileImageCropView(viewModel: viewModel, image: image) #else AssertionFailureView("not implemented") #endif @@ -47,7 +47,7 @@ final class UserProfileImageCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { #if os(iOS) - UserProfileImagePicker(viewModel: viewModel) + UserProfileImagePickerView(viewModel: viewModel) #else AssertionFailureView("not implemented") #endif diff --git a/Shared/ViewModels/UserProfileImageViewModel.swift b/Shared/ViewModels/UserProfileImageViewModel.swift index aa49eb826..adea383ac 100644 --- a/Shared/ViewModels/UserProfileImageViewModel.swift +++ b/Shared/ViewModels/UserProfileImageViewModel.swift @@ -151,7 +151,7 @@ class UserProfileImageViewModel: ViewModel, Eventful, Stateful { ) request.headers = ["Content-Type": contentType] - guard imageData.count > 30_000_000 else { + guard imageData.count < 30_000_000 else { throw JellyfinAPIError( "This profile image is too large (\(imageData.count.formatted(.byteCount(style: .file)))). The upload limit for images is 30 MB." ) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2cf701f62..0be080313 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -88,11 +88,9 @@ 4E49DED52CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */; }; 4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */; }; 4E49DED82CE5509300352DCD /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED72CE5509000352DCD /* StatusSection.swift */; }; - 4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */; }; - 4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */; }; 4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; }; 4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; }; - 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */; }; + 4E49DEE62CE5616800352DCD /* UserProfileImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE52CE5616800352DCD /* UserProfileImagePickerView.swift */; }; 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; }; 4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */; }; 4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */; }; @@ -190,8 +188,8 @@ 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */; }; 4EA78B182D2A265E0093BFCE /* RemoteImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */; }; 4EA78B1A2D2A26670093BFCE /* LocalImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */; }; - 4EA78B202D2B5AA30093BFCE /* ItemImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemImagePicker.swift */; }; - 4EA78B232D2B5CFC0093BFCE /* ImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ImageCropView.swift */; }; + 4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */; }; + 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; }; 4EA78B252D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; 4EA78B262D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; @@ -243,6 +241,9 @@ 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; 4EE767082D13403F009658F0 /* RemoteSearchResultRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */; }; 4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */; }; + 4EECA4E32D2C7D530080A863 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */; }; + 4EECA4E62D2C7D650080A863 /* PhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */; }; + 4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.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 */; }; @@ -1268,10 +1269,8 @@ 4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsPolicy.swift; sourceTree = ""; }; 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFailurePolicy.swift; sourceTree = ""; }; 4E49DED72CE5509000352DCD /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; - 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; - 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImageCropView.swift; sourceTree = ""; }; 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlayUserAccessType.swift; sourceTree = ""; }; - 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePicker.swift; sourceTree = ""; }; + 4E49DEE52CE5616800352DCD /* UserProfileImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePickerView.swift; sourceTree = ""; }; 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitLoginWindowView.swift; sourceTree = ""; }; 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalServerButton.swift; sourceTree = ""; }; 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudioEditorViewModel.swift; sourceTree = ""; }; @@ -1347,8 +1346,8 @@ 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsView.swift; sourceTree = ""; }; 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfoView.swift; sourceTree = ""; }; 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImageInfoView.swift; sourceTree = ""; }; - 4EA78B1F2D2B5A9E0093BFCE /* ItemImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePicker.swift; sourceTree = ""; }; - 4EA78B222D2B5CEF0093BFCE /* ImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCropView.swift; sourceTree = ""; }; + 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoPickerView.swift; sourceTree = ""; }; + 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCropView.swift; sourceTree = ""; }; 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCoordinator.swift; sourceTree = ""; }; 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = ""; }; 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = ""; }; @@ -1391,6 +1390,9 @@ 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResult.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 = ""; }; + 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = ""; }; + 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCropView.swift; sourceTree = ""; }; + 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImageCropView.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 = ""; }; @@ -2306,10 +2308,10 @@ 4E37F6182D17EB220022AADD /* ItemImages */ = { isa = PBXGroup; children = ( - 4EA78B1E2D2B5A960093BFCE /* ItemImagePicker */, 4E4593A42D04E4D600E277E1 /* AddItemImageView */, 4EA78B152D2A0C4A0093BFCE /* ItemImageDetailsView */, 4E4593A12D04E2A200E277E1 /* ItemImagesView */, + 4EA78B1E2D2B5A960093BFCE /* ItemPhotoPickerView */, ); path = ItemImages; sourceTree = ""; @@ -2352,8 +2354,7 @@ 4E49DEDE2CE55F7F00352DCD /* Components */ = { isa = PBXGroup; children = ( - 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */, - 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */, + 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */, ); path = Components; sourceTree = ""; @@ -2362,7 +2363,7 @@ isa = PBXGroup; children = ( 4E49DEDE2CE55F7F00352DCD /* Components */, - 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */, + 4E49DEE52CE5616800352DCD /* UserProfileImagePickerView.swift */, ); path = UserProfileImagePicker; sourceTree = ""; @@ -2723,19 +2724,19 @@ path = Components; sourceTree = ""; }; - 4EA78B1E2D2B5A960093BFCE /* ItemImagePicker */ = { + 4EA78B1E2D2B5A960093BFCE /* ItemPhotoPickerView */ = { isa = PBXGroup; children = ( 4EA78B212D2B5CDD0093BFCE /* Components */, - 4EA78B1F2D2B5A9E0093BFCE /* ItemImagePicker.swift */, + 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */, ); - path = ItemImagePicker; + path = ItemPhotoPickerView; sourceTree = ""; }; 4EA78B212D2B5CDD0093BFCE /* Components */ = { isa = PBXGroup; children = ( - 4EA78B222D2B5CEF0093BFCE /* ImageCropView.swift */, + 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */, ); path = Components; sourceTree = ""; @@ -2920,6 +2921,23 @@ path = Components; sourceTree = ""; }; + 4EECA4E12D2C7D450080A863 /* PhotoPickerView */ = { + isa = PBXGroup; + children = ( + 4EECA4E42D2C7D570080A863 /* Components */, + 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */, + ); + path = PhotoPickerView; + sourceTree = ""; + }; + 4EECA4E42D2C7D570080A863 /* Components */ = { + isa = PBXGroup; + children = ( + 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */, + ); + path = Components; + sourceTree = ""; + }; 4EED87472CBF824B002354D2 /* Components */ = { isa = PBXGroup; children = ( @@ -4037,6 +4055,7 @@ E19F6C5C28F5189300C5197E /* MediaStreamInfoView.swift */, E103DF922BCF2F23000229B2 /* MediaView */, E1EDA8D62B92C9D700F9A57E /* PagingLibraryView */, + 4EECA4E12D2C7D450080A863 /* PhotoPickerView */, E10231342BCF8A3C009D71FC /* ProgramsView */, E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */, 4EF10D4C2CE2EC5A000ED5F5 /* ResetUserPasswordView */, @@ -6009,6 +6028,7 @@ E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */, E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */, E1D90D762C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift in Sources */, + 4EECA4E62D2C7D650080A863 /* PhotoCropView.swift in Sources */, 6264E88C273850380081A12A /* Strings.swift in Sources */, E145EB252BE055AD003BF6F3 /* ServerResponse.swift in Sources */, E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */, @@ -6035,8 +6055,6 @@ E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, 4ED25CA42D07E4990010333C /* EditAccessScheduleRow.swift in Sources */, - 4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */, - 4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */, 4E90F7642CC72B1F00417C31 /* LastRunSection.swift in Sources */, 4E90F7652CC72B1F00417C31 /* EditServerTaskView.swift in Sources */, 4E90F7662CC72B1F00417C31 /* LastErrorSection.swift in Sources */, @@ -6233,10 +6251,10 @@ E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */, E1A2C154279A7D5A005EC829 /* UIApplication.swift in Sources */, - 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */, + 4E49DEE62CE5616800352DCD /* UserProfileImagePickerView.swift in Sources */, 4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */, E11C15352BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, - 4EA78B232D2B5CFC0093BFCE /* ImageCropView.swift in Sources */, + 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */, E1D8428F2933F2D900D1041A /* MediaSourceInfo.swift in Sources */, E1BDF2EC2952290200CC0294 /* AspectFillActionButton.swift in Sources */, BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, @@ -6275,6 +6293,7 @@ 4EFAC1302D1E2EB900E40880 /* EditAccessTagRow.swift in Sources */, E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */, E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.swift in Sources */, + 4EECA4E32D2C7D530080A863 /* PhotoPickerView.swift in Sources */, E1ED7FDC2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */, E149CCAD2BE6ECC8008B9331 /* Storable.swift in Sources */, E1CB75792C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */, @@ -6327,7 +6346,7 @@ E1F5CF092CB0A04500607465 /* Text.swift in Sources */, 4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */, E1B490442967E26300D3EDCE /* PersistentLogHandler.swift in Sources */, - 4EA78B202D2B5AA30093BFCE /* ItemImagePicker.swift in Sources */, + 4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */, E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */, E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */, E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */, @@ -6367,6 +6386,7 @@ E1763A712BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, E1D8429529346C6400D1041A /* BasicStepper.swift in Sources */, + 4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */, E18E01EA288747230022598C /* MovieItemView.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, E164A7F42BE4736300A54B18 /* SignOutIntervalSection.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift deleted file mode 100644 index ad16a90bb..000000000 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/Components/ImageCropView.swift +++ /dev/null @@ -1,201 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import Mantis -import SwiftUI - -extension ItemImagePicker { - - struct ImageCropView: View { - - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - - // MARK: - State, Observed, & Environment Objects - - @EnvironmentObject - private var router: ItemPhotoCoordinator.Router - - @StateObject - private var proxy: _ImageCropView.Proxy = .init() - - @ObservedObject - var viewModel: ItemImagesViewModel - - // MARK: - Image Variable - - let image: UIImage - - let type: ImageType - - // MARK: - Error State - - @State - private var error: Error? = nil - - // MARK: - Body - - var body: some View { - _ImageCropView(initialImage: image, proxy: proxy) { - viewModel.send(.uploadPhoto(image: $0, type: type)) - } - .animation(.linear(duration: 0.1), value: viewModel.state) - .interactiveDismissDisabled(viewModel.state == .updating) - .navigationBarBackButtonHidden(viewModel.state == .updating) - .topBarTrailing { - - if viewModel.state == .initial { - Button(L10n.rotate, systemImage: "rotate.right") { - proxy.rotate() - } - .foregroundStyle(.gray) - } - - if viewModel.state == .updating { - Button(L10n.cancel) { - viewModel.send(.cancel) - } - .foregroundStyle(.red) - } else { - Button { - proxy.crop() - } label: { - Text(L10n.save) - .foregroundStyle(accentColor.overlayColor) - .font(.headline) - .padding(.vertical, 5) - .padding(.horizontal, 10) - .background { - accentColor - } - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - } - } - .toolbar { - ToolbarItem(placement: .principal) { - if viewModel.state == .updating { - ProgressView() - } else { - Button(L10n.reset) { - proxy.reset() - } - .foregroundStyle(.yellow) - .disabled(viewModel.state == .updating) - } - } - } - .ignoresSafeArea() - .background { - Color.black - } - .onReceive(viewModel.events) { event in - switch event { - case let .error(eventError): - error = eventError - case .deleted: - break - case .updated: - router.dismissCoordinator() - } - } - .errorMessage($error) - } - } - - // MARK: - Square Image Crop View - - struct _ImageCropView: UIViewControllerRepresentable { - - class Proxy: ObservableObject { - - weak var cropViewController: CropViewController? - - func crop() { - cropViewController?.crop() - } - - func reset() { - cropViewController?.didSelectReset() - } - - func rotate() { - cropViewController?.didSelectClockwiseRotate() - } - } - - let initialImage: UIImage - let proxy: Proxy - let onImageCropped: (UIImage) -> Void - - func makeUIViewController(context: Context) -> some UIViewController { - var config = Mantis.Config() - - config.cropViewConfig.backgroundColor = .black.withAlphaComponent(0.9) - config.cropViewConfig.cropShapeType = .rect - config.presetFixedRatioType = .canUseMultiplePresetFixedRatio() - config.showAttachedCropToolbar = false - - let cropViewController = Mantis.cropViewController( - image: initialImage, - config: config - ) - - cropViewController.delegate = context.coordinator - context.coordinator.onImageCropped = onImageCropped - - proxy.cropViewController = cropViewController - - return cropViewController - } - - func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - class Coordinator: CropViewControllerDelegate { - - var onImageCropped: ((UIImage) -> Void)? - - func cropViewControllerDidCrop( - _ cropViewController: CropViewController, - cropped: UIImage, - transformation: Transformation, - cropInfo: CropInfo - ) { - onImageCropped?(cropped) - } - - func cropViewControllerDidCancel( - _ cropViewController: CropViewController, - original: UIImage - ) {} - - func cropViewControllerDidFailToCrop( - _ cropViewController: CropViewController, - original: UIImage - ) {} - - func cropViewControllerDidBeginResize( - _ cropViewController: CropViewController - ) {} - - func cropViewControllerDidEndResize( - _ cropViewController: Mantis.CropViewController, - original: UIImage, - cropInfo: Mantis.CropInfo - ) {} - } - } -} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift new file mode 100644 index 000000000..562235593 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift @@ -0,0 +1,69 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import Mantis +import SwiftUI + +struct ItemPhotoCropView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: ItemPhotoCoordinator.Router + + @ObservedObject + var viewModel: ItemImagesViewModel + + // MARK: - Image Variable + + let image: UIImage + + let type: ImageType + + // MARK: - Error State + + @State + private var error: Error? = nil + + // MARK: - Body + + var body: some View { + PhotoCropView( + isReady: viewModel.state == .initial, + isSaving: viewModel.state == .updating, + image: image, + cropShape: .rect, + presetRatio: .canUseMultiplePresetFixedRatio() + ) { + viewModel.send(.uploadPhoto(image: $0, type: type)) + } onCancel: { + router.dismissCoordinator() + } + .animation(.linear(duration: 0.1), value: viewModel.state) + .interactiveDismissDisabled(viewModel.state == .updating) + .navigationBarBackButtonHidden(viewModel.state == .updating) + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + error = eventError + case .deleted: + break + case .updated: + router.dismissCoordinator() + } + } + .errorMessage($error) + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/ItemImagePicker.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift similarity index 80% rename from Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/ItemImagePicker.swift rename to Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift index 8e2320a78..cbc27747e 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagePicker/ItemImagePicker.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift @@ -19,10 +19,10 @@ struct ItemImagePicker: View { // MARK: - Body var body: some View { - UserProfileImagePicker.PhotoPicker { + PhotoPickerView { + router.route(to: \.cropImage, $0) + } onCancel: { router.dismissCoordinator() - } onSelectedImage: { image in - router.route(to: \.cropImage, image) } } } diff --git a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift new file mode 100644 index 000000000..b4ab25d35 --- /dev/null +++ b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift @@ -0,0 +1,188 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Mantis +import SwiftUI + +struct PhotoCropView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - State, Observed, & Environment Objects + + @StateObject + private var proxy: _PhotoCropView.Proxy = .init() + + // MARK: - Image Variable + + var isReady: Bool + var isSaving: Bool + let image: UIImage + let cropShape: Mantis.CropShapeType + let presetRatio: Mantis.PresetFixedRatioType + let onSave: (UIImage) -> Void + let onCancel: () -> Void + + // MARK: - Error State + + @State + private var error: Error? = nil + + // MARK: - Body + + var body: some View { + _PhotoCropView( + initialImage: image, + cropShape: cropShape, + presetRatio: presetRatio, + proxy: proxy + ) { + onSave($0) + } + .topBarTrailing { + + if isReady { + Button(L10n.rotate, systemImage: "rotate.right") { + proxy.rotate() + } + .foregroundStyle(.gray) + } + + if isSaving { + Button(L10n.cancel) { + onCancel() + } + .foregroundStyle(.red) + } else { + Button { + proxy.crop() + } label: { + Text(L10n.save) + .foregroundStyle(accentColor.overlayColor) + .font(.headline) + .padding(.vertical, 5) + .padding(.horizontal, 10) + .background { + accentColor + } + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + } + .toolbar { + ToolbarItem(placement: .principal) { + if isSaving { + ProgressView() + } else { + Button(L10n.reset) { + proxy.reset() + } + .foregroundStyle(.yellow) + .disabled(isSaving) + } + } + } + .ignoresSafeArea() + .background { + Color.black + } + } +} + +// MARK: - Photo Crop View + +struct _PhotoCropView: UIViewControllerRepresentable { + + class Proxy: ObservableObject { + + weak var cropViewController: CropViewController? + + func crop() { + cropViewController?.crop() + } + + func reset() { + cropViewController?.didSelectReset() + } + + func rotate() { + cropViewController?.didSelectClockwiseRotate() + } + } + + let initialImage: UIImage + let cropShape: Mantis.CropShapeType + let presetRatio: Mantis.PresetFixedRatioType + let proxy: Proxy + let onImageCropped: (UIImage) -> Void + + func makeUIViewController(context: Context) -> some UIViewController { + var config = Mantis.Config() + + config.cropViewConfig.backgroundColor = .black.withAlphaComponent(0.9) + config.cropViewConfig.cropShapeType = cropShape + config.presetFixedRatioType = presetRatio + config.showAttachedCropToolbar = false + + let cropViewController = Mantis.cropViewController( + image: initialImage, + config: config + ) + + cropViewController.delegate = context.coordinator + context.coordinator.onImageCropped = onImageCropped + + proxy.cropViewController = cropViewController + + return cropViewController + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + class Coordinator: CropViewControllerDelegate { + + var onImageCropped: ((UIImage) -> Void)? + + func cropViewControllerDidCrop( + _ cropViewController: CropViewController, + cropped: UIImage, + transformation: Transformation, + cropInfo: CropInfo + ) { + onImageCropped?(cropped) + } + + func cropViewControllerDidCancel( + _ cropViewController: CropViewController, + original: UIImage + ) {} + + func cropViewControllerDidFailToCrop( + _ cropViewController: CropViewController, + original: UIImage + ) {} + + func cropViewControllerDidBeginResize( + _ cropViewController: CropViewController + ) {} + + func cropViewControllerDidEndResize( + _ cropViewController: Mantis.CropViewController, + original: UIImage, + cropInfo: Mantis.CropInfo + ) {} + } +} diff --git a/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift new file mode 100644 index 000000000..cb020fb70 --- /dev/null +++ b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift @@ -0,0 +1,87 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import PhotosUI +import SwiftUI + +// TODO: polish: find way to deselect image on appear +// - from popping from cropping +// TODO: polish: when image is picked, instead of loading it here +// which takes ~1-2s, show some kind of loading indicator +// on this view or push to another view that will go to crop + +struct PhotoPickerView: UIViewControllerRepresentable { + + // MARK: - Photo Picker Actions + + var onSelect: (UIImage) -> Void + var onCancel: () -> Void + + // MARK: - Initializer + + init(onSelect: @escaping (UIImage) -> Void, onCancel: @escaping () -> Void) { + self.onSelect = onSelect + self.onCancel = onCancel + } + + // MARK: - UIView Controller + + func makeUIViewController(context: Context) -> PHPickerViewController { + + var configuration = PHPickerConfiguration(photoLibrary: .shared()) + + configuration.filter = .all(of: [.images, .not(.livePhotos)]) + configuration.preferredAssetRepresentationMode = .current + configuration.selection = .ordered + configuration.selectionLimit = 1 + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = context.coordinator + + context.coordinator.onSelect = onSelect + context.coordinator.onCancel = onCancel + + return picker + } + + // MARK: - Update UIView Controller + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} + + // MARK: - Make Coordinator + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + // MARK: - Coordinator + + class Coordinator: PHPickerViewControllerDelegate { + + var onSelect: ((UIImage) -> Void)? + var onCancel: (() -> Void)? + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + + guard let image = results.first else { + onCancel?() + return + } + + let itemProvider = image.itemProvider + + if itemProvider.canLoadObject(ofClass: UIImage.self) { + itemProvider.loadObject(ofClass: UIImage.self) { image, _ in + if let image = image as? UIImage { + self.onSelect?(image) + } + } + } + } + } +} diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift b/Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift deleted file mode 100644 index 358d162db..000000000 --- a/Swiftfin/Views/UserProfileImagePicker/Components/PhotoPicker.swift +++ /dev/null @@ -1,90 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import PhotosUI -import SwiftUI - -// TODO: polish: find way to deselect image on appear -// - from popping from cropping -// TODO: polish: when image is picked, instead of loading it here -// which takes ~1-2s, show some kind of loading indicator -// on this view or push to another view that will go to crop - -extension UserProfileImagePicker { - - struct PhotoPicker: UIViewControllerRepresentable { - - // MARK: - Photo Picker Actions - - var onCancel: () -> Void - var onSelectedImage: (UIImage) -> Void - - // MARK: - Initializer - - init(onCancel: @escaping () -> Void, onSelectedImage: @escaping (UIImage) -> Void) { - self.onCancel = onCancel - self.onSelectedImage = onSelectedImage - } - - // MARK: - UIView Controller - - func makeUIViewController(context: Context) -> PHPickerViewController { - - var configuration = PHPickerConfiguration(photoLibrary: .shared()) - - configuration.filter = .all(of: [.images, .not(.livePhotos)]) - configuration.preferredAssetRepresentationMode = .current - configuration.selection = .ordered - configuration.selectionLimit = 1 - - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = context.coordinator - - context.coordinator.onCancel = onCancel - context.coordinator.onSelectedImage = onSelectedImage - - return picker - } - - // MARK: - Update UIView Controller - - func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} - - // MARK: - Make Coordinator - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - // MARK: - Coordinator - - class Coordinator: PHPickerViewControllerDelegate { - - var onCancel: (() -> Void)? - var onSelectedImage: ((UIImage) -> Void)? - - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - - guard let image = results.first else { - onCancel?() - return - } - - let itemProvider = image.itemProvider - - if itemProvider.canLoadObject(ofClass: UIImage.self) { - itemProvider.loadObject(ofClass: UIImage.self) { image, _ in - if let image = image as? UIImage { - self.onSelectedImage?(image) - } - } - } - } - } - } -} diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift b/Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift deleted file mode 100644 index 3b649b174..000000000 --- a/Swiftfin/Views/UserProfileImagePicker/Components/SquareImageCropView.swift +++ /dev/null @@ -1,198 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import Defaults -import Mantis -import SwiftUI - -extension UserProfileImagePicker { - - struct SquareImageCropView: View { - - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - - // MARK: - State, Observed, & Environment Objects - - @EnvironmentObject - private var router: UserProfileImageCoordinator.Router - - @StateObject - private var proxy: _SquareImageCropView.Proxy = .init() - - @ObservedObject - var viewModel: UserProfileImageViewModel - - // MARK: - Image Variable - - let image: UIImage - - // MARK: - Error State - - @State - private var error: Error? = nil - - // MARK: - Body - - var body: some View { - _SquareImageCropView(initialImage: image, proxy: proxy) { - viewModel.send(.upload($0)) - } - .animation(.linear(duration: 0.1), value: viewModel.state) - .interactiveDismissDisabled(viewModel.state == .uploading) - .navigationBarBackButtonHidden(viewModel.state == .uploading) - .topBarTrailing { - - if viewModel.state == .initial { - Button(L10n.rotate, systemImage: "rotate.right") { - proxy.rotate() - } - .foregroundStyle(.gray) - } - - if viewModel.state == .uploading { - Button(L10n.cancel) { - viewModel.send(.cancel) - } - .foregroundStyle(.red) - } else { - Button { - proxy.crop() - } label: { - Text(L10n.save) - .foregroundStyle(accentColor.overlayColor) - .font(.headline) - .padding(.vertical, 5) - .padding(.horizontal, 10) - .background { - accentColor - } - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - } - } - .toolbar { - ToolbarItem(placement: .principal) { - if viewModel.state == .uploading { - ProgressView() - } else { - Button(L10n.reset) { - proxy.reset() - } - .foregroundStyle(.yellow) - .disabled(viewModel.state == .uploading) - } - } - } - .ignoresSafeArea() - .background { - Color.black - } - .onReceive(viewModel.events) { event in - switch event { - case let .error(eventError): - error = eventError - case .deleted: - break - case .uploaded: - router.dismissCoordinator() - } - } - .errorMessage($error) - } - } - - // MARK: - Square Image Crop View - - struct _SquareImageCropView: UIViewControllerRepresentable { - - class Proxy: ObservableObject { - - weak var cropViewController: CropViewController? - - func crop() { - cropViewController?.crop() - } - - func reset() { - cropViewController?.didSelectReset() - } - - func rotate() { - cropViewController?.didSelectClockwiseRotate() - } - } - - let initialImage: UIImage - let proxy: Proxy - let onImageCropped: (UIImage) -> Void - - func makeUIViewController(context: Context) -> some UIViewController { - var config = Mantis.Config() - - config.cropViewConfig.backgroundColor = .black.withAlphaComponent(0.9) - config.cropViewConfig.cropShapeType = .square - config.presetFixedRatioType = .alwaysUsingOnePresetFixedRatio(ratio: 1) - config.showAttachedCropToolbar = false - - let cropViewController = Mantis.cropViewController( - image: initialImage, - config: config - ) - - cropViewController.delegate = context.coordinator - context.coordinator.onImageCropped = onImageCropped - - proxy.cropViewController = cropViewController - - return cropViewController - } - - func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - class Coordinator: CropViewControllerDelegate { - - var onImageCropped: ((UIImage) -> Void)? - - func cropViewControllerDidCrop( - _ cropViewController: CropViewController, - cropped: UIImage, - transformation: Transformation, - cropInfo: CropInfo - ) { - onImageCropped?(cropped) - } - - func cropViewControllerDidCancel( - _ cropViewController: CropViewController, - original: UIImage - ) {} - - func cropViewControllerDidFailToCrop( - _ cropViewController: CropViewController, - original: UIImage - ) {} - - func cropViewControllerDidBeginResize( - _ cropViewController: CropViewController - ) {} - - func cropViewControllerDidEndResize( - _ cropViewController: Mantis.CropViewController, - original: UIImage, - cropInfo: Mantis.CropInfo - ) {} - } - } -} diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift new file mode 100644 index 000000000..cecf78f6a --- /dev/null +++ b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift @@ -0,0 +1,67 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import Mantis +import SwiftUI + +struct UserProfileImageCropView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: UserProfileImageCoordinator.Router + + @ObservedObject + var viewModel: UserProfileImageViewModel + + // MARK: - Image Variable + + let image: UIImage + + // MARK: - Error State + + @State + private var error: Error? = nil + + // MARK: - Body + + var body: some View { + PhotoCropView( + isReady: viewModel.state == .initial, + isSaving: viewModel.state == .uploading, + image: image, + cropShape: .square, + presetRatio: .alwaysUsingOnePresetFixedRatio(ratio: 1) + ) { + viewModel.send(.upload($0)) + } onCancel: { + router.dismissCoordinator() + } + .animation(.linear(duration: 0.1), value: viewModel.state) + .interactiveDismissDisabled(viewModel.state == .uploading) + .navigationBarBackButtonHidden(viewModel.state == .uploading) + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + error = eventError + case .deleted: + break + case .uploaded: + router.dismissCoordinator() + } + } + .errorMessage($error) + } +} diff --git a/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePicker.swift b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift similarity index 79% rename from Swiftfin/Views/UserProfileImagePicker/UserProfileImagePicker.swift rename to Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift index 6ebc6875e..f707a0bac 100644 --- a/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePicker.swift +++ b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift @@ -8,7 +8,7 @@ import SwiftUI -struct UserProfileImagePicker: View { +struct UserProfileImagePickerView: View { // MARK: - Observed, & Environment Objects @@ -21,10 +21,10 @@ struct UserProfileImagePicker: View { // MARK: - Body var body: some View { - PhotoPicker { + PhotoPickerView { + router.route(to: \.cropImage, $0) + } onCancel: { router.dismissCoordinator() - } onSelectedImage: { image in - router.route(to: \.cropImage, image) } } } From 8cc8d6415904a7e48bcbda0ce3085e7cef55b039 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 15:33:49 -0700 Subject: [PATCH 25/45] 4/6 of the codefactor changes --- .../ItemImageDetailsView/Components/LocalImageInfoView.swift | 4 ++++ .../Components/RemoteImageInfoView.swift | 4 +++- .../ItemPhotoPickerView/Components/ItemPhotoCropView.swift | 5 ----- .../Views/PhotoPickerView/Components/PhotoCropView.swift | 5 ----- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift index 641c14507..c6a399c5b 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift @@ -78,6 +78,8 @@ extension ItemImageDetailsView { } } + // MARK: - Delete Button + var deleteButton: some View { ListRowButton(L10n.delete) { onDelete() @@ -88,6 +90,8 @@ extension ItemImageDetailsView { ) } + // MARK: - Body + var body: some View { List { header diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift index 38ccf1ec2..e7d77ae69 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift @@ -25,7 +25,7 @@ extension ItemImageDetailsView { @ViewBuilder private var header: some View { Section { - ImageView(URL(string: imageInfo.url ?? nil)) + ImageView(URL(string: imageInfo.url)) .placeholder { _ in Image(systemName: imageInfo.systemImage) } @@ -79,6 +79,8 @@ extension ItemImageDetailsView { } } + // MARK: - Body + var body: some View { List { header diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift index 562235593..165ea242c 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift @@ -32,11 +32,6 @@ struct ItemPhotoCropView: View { let type: ImageType - // MARK: - Error State - - @State - private var error: Error? = nil - // MARK: - Body var body: some View { diff --git a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift index b4ab25d35..7c1142c6c 100644 --- a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift +++ b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift @@ -32,11 +32,6 @@ struct PhotoCropView: View { let onSave: (UIImage) -> Void let onCancel: () -> Void - // MARK: - Error State - - @State - private var error: Error? = nil - // MARK: - Body var body: some View { From eb1570f9166d3f6722f856f1a2c88a1c0e56e889 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 16:07:24 -0700 Subject: [PATCH 26/45] Pass around the URL NOT the UIImage --- .../Coordinators/ItemImagesCoordinator.swift | 2 +- .../ItemImagesViewModel.swift | 19 ++++++--------- .../Components/LocalImageInfoView.swift | 24 +++++++++---------- .../ItemImageDetailsView.swift | 6 ++--- .../ItemImagesView/ItemImagesView.swift | 18 +++++++++----- .../Components/ItemPhotoCropView.swift | 5 ++++ .../Components/UserProfileImageCropView.swift | 2 +- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index c378c102f..e49d150ca 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -44,7 +44,7 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { AddItemImageView(viewModel: viewModel, imageType: imageType) } - func makeDeleteImage(imageInfo: (key: ImageInfo, value: UIImage)) -> NavigationViewCoordinator { + func makeDeleteImage(imageInfo: (key: ImageInfo, value: URL)) -> NavigationViewCoordinator { NavigationViewCoordinator { ItemImageDetailsView(viewModel: self.viewModel, localImageInfo: imageInfo) } diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index c2847239c..4a812ab1b 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -52,7 +52,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { @Published var item: BaseItemDto @Published - var images: [ImageInfo: UIImage] = [:] + var images: [ImageInfo: URL] = [:] // MARK: - State Management @@ -281,7 +281,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { let missingImageInfos = imageInfos.filter { !self.images.keys.contains($0) } - try await withThrowingTaskGroup(of: (ImageInfo, UIImage).self) { group in + try await withThrowingTaskGroup(of: (ImageInfo, URL).self) { group in for imageInfo in missingImageInfos { group.addTask { do { @@ -294,23 +294,18 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { imageType: imageInfo.imageType?.rawValue ?? "", parameters: parameters ) - let response = try await self.userSession.client.send(request) - // TODO: Is there a way for me to just get the Image URL so I can use - // ImageView instead of passing/storing the full images? - if let image = UIImage(data: response.value) { - return (imageInfo, image) + if let imageURL = self.userSession.client.fullURL(with: request) { + return (imageInfo, imageURL) } - } catch { - throw JellyfinAPIError("Failed to fetch image for \(imageInfo): \(error)") } throw JellyfinAPIError("Failed to fetch image for \(imageInfo)") } } - for try await (imageInfo, image) in group { + for try await (imageInfo, URL) in group { await MainActor.run { - self.images[imageInfo] = image + self.images[imageInfo] = URL } } } @@ -452,7 +447,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } return (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) } - .reduce(into: [ImageInfo: UIImage]()) { result, pair in + .reduce(into: [ImageInfo: URL]()) { result, pair in var updatedInfo = pair.key if updatedInfo.imageType == imageInfo.imageType, let index = updatedInfo.imageIndex, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift index c6a399c5b..47dc06bef 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift @@ -21,7 +21,7 @@ extension ItemImageDetailsView { // MARK: - Image Info let imageInfo: ImageInfo - let image: UIImage + let imageURL: URL // MARK: - Image Functions @@ -32,17 +32,17 @@ extension ItemImageDetailsView { @ViewBuilder private var header: some View { Section { - ZStack { - Color.secondarySystemFill - - Image(uiImage: image) - .resizable() - .scaledToFill() - } - .scaledToFit() - .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) - .frame(maxWidth: .infinity) - .accessibilityIgnoresInvertColors() + ImageView(imageURL) + .placeholder { _ in + Image(systemName: "circle") + } + .failure { + Image(systemName: "circle") + } + .scaledToFit() + .frame(maxWidth: .infinity) + .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) + .accessibilityIgnoresInvertColors() } .listRowBackground(Color.clear) .listRowCornerRadius(0) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index cce11f626..3e1a86257 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -31,7 +31,7 @@ struct ItemImageDetailsView: View { // MARK: - Image Variable - private let localImageInfo: (key: ImageInfo, value: UIImage)? + private let localImageInfo: (key: ImageInfo, value: URL)? private let remoteImageInfo: RemoteImageInfo? @@ -47,7 +47,7 @@ struct ItemImageDetailsView: View { // MARK: - Initializer - init(viewModel: ItemImagesViewModel, localImageInfo: (key: ImageInfo, value: UIImage)? = nil, remoteImageInfo: RemoteImageInfo? = nil) { + init(viewModel: ItemImagesViewModel, localImageInfo: (key: ImageInfo, value: URL)? = nil, remoteImageInfo: RemoteImageInfo? = nil) { self._viewModel = ObservedObject(wrappedValue: viewModel) self.localImageInfo = localImageInfo self.remoteImageInfo = remoteImageInfo @@ -89,7 +89,7 @@ struct ItemImageDetailsView: View { @ViewBuilder var contentView: some View { if let imageInfo = localImageInfo { - LocalImageInfoView(imageInfo: imageInfo.key, image: imageInfo.value) { + LocalImageInfoView(imageInfo: imageInfo.key, imageURL: imageInfo.value) { viewModel.send(.deleteImage(imageInfo.key)) } } else if let imageInfo = remoteImageInfo { diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 8d2e88efb..814d48026 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -117,7 +117,7 @@ struct ItemImagesView: View { ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(sortedImageArray, id: \.key) { imageData in - imageButton(imageData.value) { + imageButton(imageInfo: imageData.key, imageURL: imageData.value) { router.route(to: \.deleteImage, imageData) } } @@ -165,16 +165,22 @@ struct ItemImagesView: View { // MARK: - Image Button - private func imageButton(_ image: UIImage, onSelect: @escaping () -> Void) -> some View { + private func imageButton(imageInfo: ImageInfo, imageURL: URL?, onSelect: @escaping () -> Void) -> some View { Button(action: onSelect) { ZStack { Color.secondarySystemFill - Image(uiImage: image) - .resizable() - .scaledToFill() + ImageView(imageURL) + .placeholder { _ in + Image(systemName: "circle") + } + .failure { + Image(systemName: "circle") + } + .scaledToFit() + .frame(maxWidth: .infinity) } .scaledToFit() - .posterStyle(image.size.height > image.size.width ? .portrait : .landscape) + .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) .frame(maxHeight: 150) .shadow(radius: 4) .padding(16) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift index 165ea242c..019109591 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift @@ -32,6 +32,11 @@ struct ItemPhotoCropView: View { let type: ImageType + // MARK: - Error State + + @State + private var error: Error? + // MARK: - Body var body: some View { diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift index cecf78f6a..e3357fcf8 100644 --- a/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift +++ b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift @@ -33,7 +33,7 @@ struct UserProfileImageCropView: View { // MARK: - Error State @State - private var error: Error? = nil + private var error: Error? // MARK: - Body From d004bcfb71151421a9bd2874c409d6b8e81b08a5 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 17:21:54 -0700 Subject: [PATCH 27/45] Clean up ItemImageDetails types. --- Shared/Extensions/RatingType.swift | 21 ++++ Shared/Strings/Strings.swift | 12 ++ Swiftfin.xcodeproj/project.pbxproj | 26 ++-- .../ItemImageDetailsDeleteButton.swift | 39 ++++++ .../ItemImageDetailsDetailsSection.swift | 116 ++++++++++++++++++ .../ItemImageDetailsHeaderSection.swift | 45 +++++++ .../Components/LocalImageInfoView.swift | 103 ---------------- .../Components/RemoteImageInfoView.swift | 95 -------------- .../ItemImageDetailsView.swift | 57 ++++++++- .../ItemImagesView/ItemImagesView.swift | 4 +- Translations/en.lproj/Localizable.strings | 18 +++ 11 files changed, 323 insertions(+), 213 deletions(-) create mode 100644 Shared/Extensions/RatingType.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift create mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift delete mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift delete mode 100644 Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift diff --git a/Shared/Extensions/RatingType.swift b/Shared/Extensions/RatingType.swift new file mode 100644 index 000000000..d3cd23eae --- /dev/null +++ b/Shared/Extensions/RatingType.swift @@ -0,0 +1,21 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI + +extension RatingType: @retroactive Displayable { + + var displayTitle: String { + switch self { + case .score: + return L10n.score + case .likes: + return L10n.likes + } + } +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index f5bc7d71d..f7bf09735 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -466,6 +466,8 @@ internal enum L10n { internal static let devices = L10n.tr("Localizable", "devices", fallback: "Devices") /// Digital internal static let digital = L10n.tr("Localizable", "digital", fallback: "Digital") + /// Dimensions + internal static let dimensions = L10n.tr("Localizable", "dimensions", fallback: "Dimensions") /// Direct Play internal static let direct = L10n.tr("Localizable", "direct", fallback: "Direct Play") /// Plays content in its original format. May cause playback issues on unsupported media types. @@ -628,6 +630,10 @@ internal enum L10n { internal static let illustrator = L10n.tr("Localizable", "illustrator", fallback: "Illustrator") /// Images internal static let images = L10n.tr("Localizable", "images", fallback: "Images") + /// Image source + internal static let imageSource = L10n.tr("Localizable", "imageSource", fallback: "Image source") + /// Index + internal static let index = L10n.tr("Localizable", "index", fallback: "Index") /// Indicators internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators") /// Inker @@ -700,6 +706,8 @@ internal enum L10n { internal static let light = L10n.tr("Localizable", "light", fallback: "Light") /// Liked Items internal static let likedItems = L10n.tr("Localizable", "likedItems", fallback: "Liked Items") + /// Likes + internal static let likes = L10n.tr("Localizable", "likes", fallback: "Likes") /// List internal static let list = L10n.tr("Localizable", "list", fallback: "List") /// Live TV @@ -1070,6 +1078,8 @@ internal enum L10n { internal static let saveUserWithoutAuthDescription = L10n.tr("Localizable", "saveUserWithoutAuthDescription", fallback: "Save the user to this device without any local authentication.") /// Schedule already exists internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists") + /// Score + internal static let score = L10n.tr("Localizable", "score", fallback: "Score") /// Scrub Current Time internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time") /// Search @@ -1382,6 +1392,8 @@ internal enum L10n { internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding") /// Some views may need an app restart to update. internal static let viewsMayRequireRestart = L10n.tr("Localizable", "viewsMayRequireRestart", fallback: "Some views may need an app restart to update.") + /// Votes + internal static let votes = L10n.tr("Localizable", "votes", fallback: "Votes") /// Weekday internal static let weekday = L10n.tr("Localizable", "weekday", fallback: "Weekday") /// Weekend diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 95a6e44c2..9df6e8610 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -186,8 +186,6 @@ 4EA78B102D29B0880093BFCE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B0E2D29B0820093BFCE /* Request.swift */; }; 4EA78B132D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */; }; 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */; }; - 4EA78B182D2A265E0093BFCE /* RemoteImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */; }; - 4EA78B1A2D2A26670093BFCE /* LocalImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */; }; 4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */; }; 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; }; 4EA78B252D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; @@ -244,6 +242,11 @@ 4EECA4E32D2C7D530080A863 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */; }; 4EECA4E62D2C7D650080A863 /* PhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */; }; 4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */; }; + 4EECA4EF2D2C9B310080A863 /* ItemImageDetailsHeaderSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4EE2D2C9B260080A863 /* ItemImageDetailsHeaderSection.swift */; }; + 4EECA4F12D2C9E860080A863 /* ItemImageDetailsDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F02D2C9E7B0080A863 /* ItemImageDetailsDetailsSection.swift */; }; + 4EECA4F32D2CA5A10080A863 /* ItemImageDetailsDeleteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F22D2CA59B0080A863 /* ItemImageDetailsDeleteButton.swift */; }; + 4EECA4F52D2CAA380080A863 /* RatingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F42D2CAA350080A863 /* RatingType.swift */; }; + 4EECA4F62D2CAA380080A863 /* RatingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F42D2CAA350080A863 /* RatingType.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 */; }; @@ -1342,8 +1345,6 @@ 4EA78B0E2D29B0820093BFCE /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesCoordinator.swift; sourceTree = ""; }; 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsView.swift; sourceTree = ""; }; - 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfoView.swift; sourceTree = ""; }; - 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImageInfoView.swift; sourceTree = ""; }; 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoPickerView.swift; sourceTree = ""; }; 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCropView.swift; sourceTree = ""; }; 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCoordinator.swift; sourceTree = ""; }; @@ -1391,6 +1392,10 @@ 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = ""; }; 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCropView.swift; sourceTree = ""; }; 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImageCropView.swift; sourceTree = ""; }; + 4EECA4EE2D2C9B260080A863 /* ItemImageDetailsHeaderSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsHeaderSection.swift; sourceTree = ""; }; + 4EECA4F02D2C9E7B0080A863 /* ItemImageDetailsDetailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsDetailsSection.swift; sourceTree = ""; }; + 4EECA4F22D2CA59B0080A863 /* ItemImageDetailsDeleteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsDeleteButton.swift; sourceTree = ""; }; + 4EECA4F42D2CAA350080A863 /* RatingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingType.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 = ""; }; @@ -2715,8 +2720,9 @@ 4EA78B1B2D2A266A0093BFCE /* Components */ = { isa = PBXGroup; children = ( - 4EA78B192D2A26600093BFCE /* LocalImageInfoView.swift */, - 4EA78B172D2A26560093BFCE /* RemoteImageInfoView.swift */, + 4EECA4F22D2CA59B0080A863 /* ItemImageDetailsDeleteButton.swift */, + 4EECA4F02D2C9E7B0080A863 /* ItemImageDetailsDetailsSection.swift */, + 4EECA4EE2D2C9B260080A863 /* ItemImageDetailsHeaderSection.swift */, ); path = Components; sourceTree = ""; @@ -3512,6 +3518,7 @@ E1A505692D0B733F007EE305 /* Optional.swift */, E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */, E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */, + 4EECA4F42D2CAA350080A863 /* RatingType.swift */, E1B5861129E32EEF00E45D6E /* Sequence.swift */, E145EB442BE0AD4E003BF6F3 /* Set.swift */, 621338922660107500A81A2A /* String.swift */, @@ -5544,6 +5551,7 @@ E1E2F8462B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */, E1575E80293E77CF001665B1 /* VideoPlayerViewModel.swift in Sources */, E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */, + 4EECA4F62D2CAA380080A863 /* RatingType.swift in Sources */, E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */, E12CC1CB28D1333400678D5D /* CinematicResumeItemView.swift in Sources */, 4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */, @@ -5840,7 +5848,6 @@ 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */, - 4EA78B182D2A265E0093BFCE /* RemoteImageInfoView.swift in Sources */, E1F5CF052CB09EA000607465 /* CurrentDate.swift in Sources */, E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */, 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */, @@ -5996,6 +6003,7 @@ E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */, E1C8CE5B28FE512400DF5D7B /* CGPoint.swift in Sources */, + 4EECA4F52D2CAA380080A863 /* RatingType.swift in Sources */, 4E49DECF2CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */, E18ACA922A15A32F00BB4F35 /* (null) in Sources */, E1A3E4C92BB74EA3005C59F8 /* LoadingCard.swift in Sources */, @@ -6083,6 +6091,7 @@ E1BE1CEA2BDB5AFE008176A9 /* UserGridButton.swift in Sources */, E1401CB129386C9200E8B599 /* UIColor.swift in Sources */, E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */, + 4EECA4F32D2CA5A10080A863 /* ItemImageDetailsDeleteButton.swift in Sources */, E18E01AB288746AF0022598C /* PillHStack.swift in Sources */, E19070492C84F2BB0004600E /* ButtonStyle-iOS.swift in Sources */, E1401CAB2938140A00E8B599 /* LightAppIcon.swift in Sources */, @@ -6191,7 +6200,6 @@ 4E35CE612CBED3F300DBD886 /* TimeLimitSection.swift in Sources */, E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */, - 4EA78B1A2D2A26670093BFCE /* LocalImageInfoView.swift in Sources */, E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */, 4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */, E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */, @@ -6360,6 +6368,7 @@ C46DD8DC2A8DC3420046A504 /* LiveVideoPlayer.swift in Sources */, E11BDF972B865F550045C54A /* ItemTag.swift in Sources */, E1D4BF8A2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */, + 4EECA4F12D2C9E860080A863 /* ItemImageDetailsDetailsSection.swift in Sources */, E1D37F482B9C648E00343D2B /* MaxHeightText.swift in Sources */, BD3957752C112A330078CEF8 /* ButtonSection.swift in Sources */, E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, @@ -6409,6 +6418,7 @@ E13DD4022717EE79009D4DAF /* SelectUserCoordinator.swift in Sources */, E11245B128D919CD00D8A977 /* Overlay.swift in Sources */, E145EB4D2BE1688E003BF6F3 /* SwiftinStore+UserState.swift in Sources */, + 4EECA4EF2D2C9B310080A863 /* ItemImageDetailsHeaderSection.swift in Sources */, 53EE24E6265060780068F029 /* SearchView.swift in Sources */, E164A8152BE58C2F00A54B18 /* V2AnyData.swift in Sources */, E1DC9841296DEBD800982F06 /* WatchedIndicator.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift new file mode 100644 index 000000000..df6a96c58 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift @@ -0,0 +1,39 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + + struct DeleteButton: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - Delete Action + + let onDelete: () -> Void + + // MARK: - Header + + @ViewBuilder + var body: some View { + ListRowButton(L10n.delete) { + onDelete() + } + .foregroundStyle( + accentColor.overlayColor, + .red + ) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift new file mode 100644 index 000000000..da1a50d1d --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift @@ -0,0 +1,116 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + + struct DetailsSection: View { + + let imageID: Int? + let imageURL: URL? + let imageIndex: Int? + let imageLanguage: String? + let imageWidth: Int? + let imageHeight: Int? + let provider: String? + let rating: Double? + let ratingType: RatingType? + let ratingVotes: Int? + + // MARK: - Initializer + + init( + imageID: Int? = nil, + imageURL: URL? = nil, + imageIndex: Int? = nil, + imageLanguage: String? = nil, + imageWidth: Int? = nil, + imageHeight: Int? = nil, + provider: String? = nil, + rating: Double? = nil, + ratingType: RatingType? = nil, + ratingVotes: Int? = nil + ) { + self.imageID = imageID + self.imageURL = imageURL + self.imageIndex = imageIndex + self.imageLanguage = imageLanguage + self.imageWidth = imageWidth + self.imageHeight = imageHeight + self.provider = provider + self.rating = rating + self.ratingType = ratingType + self.ratingVotes = ratingVotes + } + + // MARK: - Body + + @ViewBuilder + var body: some View { + Section(L10n.details) { + if let provider { + TextPairView(leading: L10n.provider, trailing: provider) + } + + if let imageID { + TextPairView(leading: L10n.id, trailing: imageID.description) + } + + if let imageIndex { + TextPairView(leading: L10n.index, trailing: imageIndex.description) + } + + if let imageLanguage { + TextPairView(leading: L10n.language, trailing: imageLanguage) + } + + if let imageWidth, let imageHeight { + TextPairView( + leading: L10n.dimensions, + trailing: "\(imageWidth) x \(imageHeight)" + ) + } + } + + if let rating { + Section(L10n.ratings) { + TextPairView(leading: L10n.rating, trailing: rating.formatted(.number.precision(.fractionLength(2)))) + + if let ratingType { + TextPairView(leading: L10n.rating, trailing: ratingType.displayTitle) + } + + if let ratingVotes { + TextPairView(leading: L10n.votes, trailing: ratingVotes.description) + } + } + } + + if let imageURL = imageURL { + Section { + Button { + UIApplication.shared.open(imageURL) + } label: { + HStack { + Text(L10n.imageSource) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(.primary) + + Image(systemName: "arrow.up.forward") + .font(.body.weight(.regular)) + .foregroundColor(.secondary) + } + .foregroundStyle(.primary, .secondary) + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift new file mode 100644 index 000000000..86650a753 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift @@ -0,0 +1,45 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + + struct HeaderSection: View { + + // MARK: - Image Info + + let imageURL: URL? + + let imageType: PosterDisplayType + + // MARK: - Header + + @ViewBuilder + var body: some View { + Section { + ImageView(imageURL) + .placeholder { _ in + Image(systemName: "circle") + } + .failure { + Image(systemName: "circle") + } + .scaledToFill() + .frame(maxWidth: .infinity) + .posterStyle(imageType) + .accessibilityIgnoresInvertColors() + } + .listRowBackground(Color.clear) + .listRowCornerRadius(0) + .listRowInsets(.zero) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift deleted file mode 100644 index 47dc06bef..000000000 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/LocalImageInfoView.swift +++ /dev/null @@ -1,103 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import SwiftUI - -extension ItemImageDetailsView { - struct LocalImageInfoView: View { - - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - - // MARK: - Image Info - - let imageInfo: ImageInfo - let imageURL: URL - - // MARK: - Image Functions - - let onDelete: () -> Void - - // MARK: - Header - - @ViewBuilder - private var header: some View { - Section { - ImageView(imageURL) - .placeholder { _ in - Image(systemName: "circle") - } - .failure { - Image(systemName: "circle") - } - .scaledToFit() - .frame(maxWidth: .infinity) - .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) - .accessibilityIgnoresInvertColors() - } - .listRowBackground(Color.clear) - .listRowCornerRadius(0) - .listRowInsets(.zero) - } - - // MARK: - Details - - @ViewBuilder - private var details: some View { - Section(L10n.details) { - - TextPairView(leading: L10n.id, trailing: imageInfo.id.description) - - if let index = imageInfo.imageIndex { - TextPairView(leading: "Index", trailing: index.description) - } - - if let imageTag = imageInfo.imageTag { - TextPairView(leading: L10n.tag, trailing: imageTag) - } - - if let width = imageInfo.width, let height = imageInfo.height { - TextPairView(leading: "Dimensions", trailing: "\(width) x \(height)") - } - - if let path = imageInfo.path { - TextPairView(leading: "Path", trailing: path) - .onSubmit { - UIApplication.shared.open(URL(string: path)!) - } - } - } - } - - // MARK: - Delete Button - - var deleteButton: some View { - ListRowButton(L10n.delete) { - onDelete() - } - .foregroundStyle( - accentColor.overlayColor, - .red - ) - } - - // MARK: - Body - - var body: some View { - List { - header - details - deleteButton - } - } - } -} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift deleted file mode 100644 index e7d77ae69..000000000 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/RemoteImageInfoView.swift +++ /dev/null @@ -1,95 +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) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -extension ItemImageDetailsView { - struct RemoteImageInfoView: View { - - // MARK: - Image Info - - let imageInfo: RemoteImageInfo - - // MARK: - Image Functions - - let onSave: () -> Void - - // MARK: - Header - - @ViewBuilder - private var header: some View { - Section { - ImageView(URL(string: imageInfo.url)) - .placeholder { _ in - Image(systemName: imageInfo.systemImage) - } - .failure { - Image(systemName: imageInfo.systemImage) - } - .scaledToFit() - .frame(maxWidth: .infinity) - .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) - .accessibilityIgnoresInvertColors() - } - .listRowBackground(Color.clear) - .listRowCornerRadius(0) - .listRowInsets(.zero) - } - - // MARK: - Details - - @ViewBuilder - private var details: some View { - Section(L10n.details) { - - if let providerName = imageInfo.providerName { - TextPairView(leading: L10n.provider, trailing: providerName) - } - - TextPairView(leading: L10n.id, trailing: imageInfo.id.description) - - if let communityRating = imageInfo.communityRating { - TextPairView(leading: L10n.rating, trailing: communityRating.description) - } - - if let voteCount = imageInfo.voteCount { - TextPairView(leading: "Votes", trailing: voteCount.description) - } - - if let language = imageInfo.language { - TextPairView(leading: L10n.language, trailing: language) - } - - if let width = imageInfo.width, let height = imageInfo.height { - TextPairView(leading: "Dimensions", trailing: "\(width) x \(height)") - } - - if let url = imageInfo.url { - TextPairView(leading: L10n.url, trailing: url) - .onSubmit { - UIApplication.shared.open(URL(string: url)!) - } - } - } - } - - // MARK: - Body - - var body: some View { - List { - header - details - } - .topBarTrailing { - Button(L10n.save, action: onSave) - .buttonStyle(.toolbarPill) - } - } - } -} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index 3e1a86257..280937fc4 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -89,15 +89,60 @@ struct ItemImageDetailsView: View { @ViewBuilder var contentView: some View { if let imageInfo = localImageInfo { - LocalImageInfoView(imageInfo: imageInfo.key, imageURL: imageInfo.value) { - viewModel.send(.deleteImage(imageInfo.key)) - } + localImageView(imageInfo: imageInfo.key, imageURL: imageInfo.value) } else if let imageInfo = remoteImageInfo { - RemoteImageInfoView(imageInfo: imageInfo) { - viewModel.send(.setImage(imageInfo)) - } + remoteImageView(imageInfo: imageInfo) } else { ErrorView(error: JellyfinAPIError("No image provided.")) } } + + @ViewBuilder + func localImageView(imageInfo: ImageInfo, imageURL: URL) -> some View { + List { + HeaderSection( + imageURL: imageURL, + imageType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape + ) + DetailsSection( + imageID: imageInfo.id, + imageURL: imageURL, + imageIndex: imageInfo.imageIndex, + imageWidth: imageInfo.width, + imageHeight: imageInfo.height + ) + DeleteButton { + viewModel.send(.deleteImage(imageInfo)) + } + } + } + + @ViewBuilder + func remoteImageView(imageInfo: RemoteImageInfo) -> some View { + let imageURL = URL(string: imageInfo.url) + + List { + HeaderSection( + imageURL: URL(string: imageInfo.url), + imageType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape + ) + DetailsSection( + imageID: imageInfo.id, + imageURL: imageURL, + imageLanguage: imageInfo.language, + imageWidth: imageInfo.width, + imageHeight: imageInfo.height, + provider: imageInfo.providerName, + rating: imageInfo.communityRating, + ratingType: imageInfo.ratingType, + ratingVotes: imageInfo.voteCount + ) + } + .topBarTrailing { + Button(L10n.save) { + viewModel.send(.setImage(imageInfo)) + } + .buttonStyle(.toolbarPill) + } + } } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 814d48026..1c7d4c48c 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -69,9 +69,11 @@ struct ItemImagesView: View { case let .success(urls): if let file = urls.first, let type = selectedType { viewModel.send(.uploadImage(file: file, type: type)) + selectedType = nil } case let .failure(fileError): - self.error = fileError + error = fileError + selectedType = nil } } .onReceive(viewModel.events) { event in diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index df3160210..db9ec98df 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -655,6 +655,9 @@ /// Digital "digital" = "Digital"; +/// Dimensions +"dimensions" = "Dimensions"; + /// Direct Play "direct" = "Direct Play"; @@ -886,6 +889,12 @@ /// Images "images" = "Images"; +/// Image source +"imageSource" = "Image source"; + +/// Index +"index" = "Index"; + /// Indicators "indicators" = "Indicators"; @@ -985,6 +994,9 @@ /// Liked Items "likedItems" = "Liked Items"; +/// Likes +"likes" = "Likes"; + /// List "list" = "List"; @@ -1531,6 +1543,9 @@ /// Schedule already exists "scheduleAlreadyExists" = "Schedule already exists"; +/// Score +"score" = "Score"; + /// Scrub Current Time "scrubCurrentTime" = "Scrub Current Time"; @@ -1981,6 +1996,9 @@ /// Some views may need an app restart to update. "viewsMayRequireRestart" = "Some views may need an app restart to update."; +/// Votes +"votes" = "Votes"; + /// Weekday "weekday" = "Weekday"; From 43364b7a8124bffeddfed0fcd1b84a3b1a7f64c9 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 17:28:29 -0700 Subject: [PATCH 28/45] Make sure the ImageView mirrors the real shape of the image. Posters should be uniform but this is the selection for the image so the dimensions are important to demonstrate. --- .../Components/ItemImageDetailsHeaderSection.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift index 86650a753..a5cba24e6 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift @@ -32,11 +32,11 @@ extension ItemImageDetailsView { .failure { Image(systemName: "circle") } - .scaledToFill() + .scaledToFit() .frame(maxWidth: .infinity) - .posterStyle(imageType) - .accessibilityIgnoresInvertColors() } + .scaledToFit() + .posterStyle(imageType) .listRowBackground(Color.clear) .listRowCornerRadius(0) .listRowInsets(.zero) From 31b4da68d6b5ba2fcf99f83057602c5d3328e817 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 17:32:15 -0700 Subject: [PATCH 29/45] Rating Type label. --- .../Components/ItemImageDetailsDetailsSection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift index da1a50d1d..ce78bbb93 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift @@ -84,7 +84,7 @@ extension ItemImageDetailsView { TextPairView(leading: L10n.rating, trailing: rating.formatted(.number.precision(.fractionLength(2)))) if let ratingType { - TextPairView(leading: L10n.rating, trailing: ratingType.displayTitle) + TextPairView(leading: L10n.type, trailing: ratingType.displayTitle) } if let ratingVotes { From d17d41683ee49c71e59f87d6e89632a273c4c4de Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 19:32:55 -0700 Subject: [PATCH 30/45] Delete confirmation dialog. --- .../ItemImageDetailsDeleteButton.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift index df6a96c58..3538bbf9d 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift @@ -23,17 +23,36 @@ extension ItemImageDetailsView { let onDelete: () -> Void + // MARK: - Dialog State + + @State + var isPresentingConfirmation: Bool = false + // MARK: - Header @ViewBuilder var body: some View { ListRowButton(L10n.delete) { - onDelete() + isPresentingConfirmation = true } .foregroundStyle( accentColor.overlayColor, .red ) + .confirmationDialog( + L10n.delete, + isPresented: $isPresentingConfirmation, + titleVisibility: .visible + ) { + Button(L10n.delete, role: .destructive) { + onDelete() + } + Button(L10n.cancel, role: .cancel) { + isPresentingConfirmation = false + } + } message: { + Text(L10n.deleteItemConfirmationMessage) + } } } } From a94fc644e242efbd894bf7bc6dfc0c6ae69e03ad Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 20:39:42 -0700 Subject: [PATCH 31/45] Remove double sizing. Remove Unused ViewModel. Change PhotoPicker to a checkmark instead a 1. Since there is only ever one picture selected, no need to count the images. --- Shared/Coordinators/UserProfileImageCoordinator.swift | 2 +- .../Components/ItemImageDetailsHeaderSection.swift | 3 +-- Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift | 2 +- .../UserProfileImagePicker/UserProfileImagePickerView.swift | 3 --- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Shared/Coordinators/UserProfileImageCoordinator.swift b/Shared/Coordinators/UserProfileImageCoordinator.swift index 393db01cf..520d23ab3 100644 --- a/Shared/Coordinators/UserProfileImageCoordinator.swift +++ b/Shared/Coordinators/UserProfileImageCoordinator.swift @@ -47,7 +47,7 @@ final class UserProfileImageCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { #if os(iOS) - UserProfileImagePickerView(viewModel: viewModel) + UserProfileImagePickerView() #else AssertionFailureView("not implemented") #endif diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift index a5cba24e6..ccf43d699 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift @@ -32,11 +32,10 @@ extension ItemImageDetailsView { .failure { Image(systemName: "circle") } - .scaledToFit() - .frame(maxWidth: .infinity) } .scaledToFit() .posterStyle(imageType) + .frame(maxWidth: .infinity) .listRowBackground(Color.clear) .listRowCornerRadius(0) .listRowInsets(.zero) diff --git a/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift index cb020fb70..f9c856b0e 100644 --- a/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift +++ b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift @@ -37,7 +37,7 @@ struct PhotoPickerView: UIViewControllerRepresentable { configuration.filter = .all(of: [.images, .not(.livePhotos)]) configuration.preferredAssetRepresentationMode = .current - configuration.selection = .ordered + configuration.selection = .default configuration.selectionLimit = 1 let picker = PHPickerViewController(configuration: configuration) diff --git a/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift index f707a0bac..4d4860204 100644 --- a/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift +++ b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift @@ -15,9 +15,6 @@ struct UserProfileImagePickerView: View { @EnvironmentObject private var router: UserProfileImageCoordinator.Router - @ObservedObject - var viewModel: UserProfileImageViewModel - // MARK: - Body var body: some View { From 0832c7a6f6067ded43312459bc9d7b29412a5cfe Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 23:41:18 -0700 Subject: [PATCH 32/45] Get the image URL as needed. No more Truples. Localize ImageTypes. --- .../Coordinators/ItemImagesCoordinator.swift | 2 +- Shared/Extensions/JellyfinAPI/ImageInfo.swift | 39 ++++++++- Shared/Extensions/JellyfinAPI/ImageType.swift | 44 ++++++++++ .../JellyfinAPI/RemoteImageInfo.swift | 8 +- Shared/Strings/Strings.swift | 24 +++++ .../ItemImagesViewModel.swift | 87 ++++++------------- Swiftfin.xcodeproj/project.pbxproj | 6 ++ .../ItemImageDetailsDetailsSection.swift | 15 +--- .../ItemImageDetailsHeaderSection.swift | 9 +- .../ItemImageDetailsView.swift | 38 +++++--- .../ItemImagesView/ItemImagesView.swift | 68 ++++++++++----- Translations/en.lproj/Localizable.strings | 36 ++++++++ 12 files changed, 261 insertions(+), 115 deletions(-) create mode 100644 Shared/Extensions/JellyfinAPI/ImageType.swift diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index e49d150ca..8b34f44c9 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -44,7 +44,7 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { AddItemImageView(viewModel: viewModel, imageType: imageType) } - func makeDeleteImage(imageInfo: (key: ImageInfo, value: URL)) -> NavigationViewCoordinator { + func makeDeleteImage(imageInfo: ImageInfo) -> NavigationViewCoordinator { NavigationViewCoordinator { ItemImageDetailsView(viewModel: self.viewModel, localImageInfo: imageInfo) } diff --git a/Shared/Extensions/JellyfinAPI/ImageInfo.swift b/Shared/Extensions/JellyfinAPI/ImageInfo.swift index e2374f2df..62d2c79f7 100644 --- a/Shared/Extensions/JellyfinAPI/ImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ImageInfo.swift @@ -9,9 +9,46 @@ import Foundation import JellyfinAPI -extension ImageInfo: @retroactive Identifiable { +extension ImageInfo: @retroactive Identifiable, Poster { public var id: Int { hashValue } + + // TODO: Remove if not using PosterHStack -> + var unwrappedIDHashOrZero: Int { + id + } + + var displayTitle: String { + if let imageIndex { + "\(imageType?.displayTitle ?? L10n.unknown)-\(imageIndex)" + } else { + imageType?.displayTitle ?? L10n.unknown + } + } + + var systemImage: String { + "circle" + } + // TODO: <- Remove if not using PosterHStack +} + +extension ImageInfo { + + func itemImageSource(itemID: String, client: JellyfinClient) -> ImageSource { + let parameters = Paths.GetItemImageParameters( + tag: imageTag, + imageIndex: imageIndex + ) + let request = Paths.getItemImage( + itemID: itemID, + imageType: imageType?.rawValue ?? "", + parameters: parameters + ) + + let itemImageURL = client.fullURL(with: request) + + return ImageSource(url: itemImageURL) + } } diff --git a/Shared/Extensions/JellyfinAPI/ImageType.swift b/Shared/Extensions/JellyfinAPI/ImageType.swift new file mode 100644 index 000000000..d90ae1d2f --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/ImageType.swift @@ -0,0 +1,44 @@ +// +// 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) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension ImageType: Displayable { + + var displayTitle: String { + switch self { + case .primary: + return L10n.primary + case .art: + return L10n.art + case .backdrop: + return L10n.backdrop + case .banner: + return L10n.banner + case .logo: + return L10n.logo + case .thumb: + return L10n.thumb + case .disc: + return L10n.disc + case .box: + return L10n.box + case .screenshot: + return L10n.screenshot + case .menu: + return L10n.menu + case .chapter: + return L10n.chapter + case .boxRear: + return L10n.boxRear + case .profile: + return L10n.profile + } + } +} diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift index e308e1861..17bbd2c9c 100644 --- a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift @@ -13,15 +13,15 @@ import SwiftUI extension RemoteImageInfo: @retroactive Identifiable, Poster { var displayTitle: String { - self.providerName ?? L10n.unknown + providerName ?? L10n.unknown } var unwrappedIDHashOrZero: Int { - self.id + id } var subtitle: String? { - self.language + language } var systemImage: String { @@ -29,6 +29,6 @@ extension RemoteImageInfo: @retroactive Identifiable, Poster { } public var id: Int { - self.hashValue + hashValue } } diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index f7bf09735..de7008477 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -120,6 +120,8 @@ internal enum L10n { internal static let applicationName = L10n.tr("Localizable", "applicationName", fallback: "Application Name") /// Arranger internal static let arranger = L10n.tr("Localizable", "arranger", fallback: "Arranger") + /// Art + internal static let art = L10n.tr("Localizable", "art", fallback: "Art") /// Artist internal static let artist = L10n.tr("Localizable", "artist", fallback: "Artist") /// Aspect Fill @@ -156,6 +158,10 @@ internal enum L10n { internal static let autoPlay = L10n.tr("Localizable", "autoPlay", fallback: "Auto Play") /// Back internal static let back = L10n.tr("Localizable", "back", fallback: "Back") + /// Backdrop + internal static let backdrop = L10n.tr("Localizable", "backdrop", fallback: "Backdrop") + /// Banner + internal static let banner = L10n.tr("Localizable", "banner", fallback: "Banner") /// Bar Buttons internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons") /// Behavior @@ -222,6 +228,10 @@ internal enum L10n { internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue") /// Books internal static let books = L10n.tr("Localizable", "books", fallback: "Books") + /// Box + internal static let box = L10n.tr("Localizable", "box", fallback: "Box") + /// BoxRear + internal static let boxRear = L10n.tr("Localizable", "boxRear", fallback: "BoxRear") /// Bugs and Features internal static let bugsAndFeatures = L10n.tr("Localizable", "bugsAndFeatures", fallback: "Bugs and Features") /// Buttons @@ -242,6 +252,8 @@ internal enum L10n { internal static let changePin = L10n.tr("Localizable", "changePin", fallback: "Change Pin") /// Channels internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels") + /// Chapter + internal static let chapter = L10n.tr("Localizable", "chapter", fallback: "Chapter") /// Chapters internal static let chapters = L10n.tr("Localizable", "chapters", fallback: "Chapters") /// Chapter Slider @@ -482,6 +494,8 @@ internal enum L10n { internal static let directStream = L10n.tr("Localizable", "directStream", fallback: "Direct Stream") /// Disabled internal static let disabled = L10n.tr("Localizable", "disabled", fallback: "Disabled") + /// Disc + internal static let disc = L10n.tr("Localizable", "disc", fallback: "Disc") /// Disclaimer internal static let disclaimer = L10n.tr("Localizable", "disclaimer", fallback: "Disclaimer") /// Dismiss @@ -730,6 +744,8 @@ internal enum L10n { internal static let lockedFields = L10n.tr("Localizable", "lockedFields", fallback: "Locked Fields") /// Locked users internal static let lockedUsers = L10n.tr("Localizable", "lockedUsers", fallback: "Locked users") + /// Logo + internal static let logo = L10n.tr("Localizable", "logo", fallback: "Logo") /// Logs internal static let logs = L10n.tr("Localizable", "logs", fallback: "Logs") /// Access the Jellyfin server logs for troubleshooting and monitoring purposes. @@ -770,6 +786,8 @@ internal enum L10n { internal static let mediaPlayback = L10n.tr("Localizable", "mediaPlayback", fallback: "Media playback") /// Mbps internal static let megabitsPerSecond = L10n.tr("Localizable", "megabitsPerSecond", fallback: "Mbps") + /// Menu + internal static let menu = L10n.tr("Localizable", "menu", fallback: "Menu") /// Menu Buttons internal static let menuButtons = L10n.tr("Localizable", "menuButtons", fallback: "Menu Buttons") /// Metadata @@ -938,6 +956,8 @@ internal enum L10n { 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 + internal static let profile = L10n.tr("Localizable", "profile", fallback: "Profile") /// Profile Image internal static let profileImage = L10n.tr("Localizable", "profileImage", fallback: "Profile Image") /// Profiles @@ -1080,6 +1100,8 @@ internal enum L10n { internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists") /// Score internal static let score = L10n.tr("Localizable", "score", fallback: "Score") + /// Screenshot + internal static let screenshot = L10n.tr("Localizable", "screenshot", fallback: "Screenshot") /// Scrub Current Time internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time") /// Search @@ -1264,6 +1286,8 @@ internal enum L10n { internal static let terabitsPerSecond = L10n.tr("Localizable", "terabitsPerSecond", fallback: "Tbps") /// Test Size internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size") + /// Thumb + internal static let thumb = L10n.tr("Localizable", "thumb", fallback: "Thumb") /// Time internal static let time = L10n.tr("Localizable", "time", fallback: "Time") /// Time Limit diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 4a812ab1b..7e777c0bc 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -52,7 +52,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { @Published var item: BaseItemDto @Published - var images: [ImageInfo: URL] = [:] + var images: IdentifiedArray = [] // MARK: - State Management @@ -266,48 +266,18 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { private func getAllImages() async throws { guard let itemID = item.id else { return } - let imageRequest = Paths.getItemImageInfos(itemID: itemID) - let imageResponse = try await self.userSession.client.send(imageRequest) - let imageInfos = imageResponse.value - - let currentImageInfos = Set(self.images.keys) - let newImageInfos = Set(imageInfos) - - guard currentImageInfos != newImageInfos else { return } + let request = Paths.getItemImageInfos(itemID: itemID) + let response = try await self.userSession.client.send(request) await MainActor.run { - self.images = self.images.filter { newImageInfos.contains($0.key) } - } - - let missingImageInfos = imageInfos.filter { !self.images.keys.contains($0) } - - try await withThrowingTaskGroup(of: (ImageInfo, URL).self) { group in - for imageInfo in missingImageInfos { - group.addTask { - do { - let parameters = Paths.GetItemImageParameters( - tag: imageInfo.imageTag ?? "", - imageIndex: imageInfo.imageIndex - ) - let request = Paths.getItemImage( - itemID: itemID, - imageType: imageInfo.imageType?.rawValue ?? "", - parameters: parameters - ) - - if let imageURL = self.userSession.client.fullURL(with: request) { - return (imageInfo, imageURL) - } - } - throw JellyfinAPIError("Failed to fetch image for \(imageInfo)") + let updatedImages = response.value.sorted { + if $0.imageType == $1.imageType { + return $0.imageIndex ?? 0 < $1.imageIndex ?? 0 } + return $0.imageType?.rawValue ?? "" < $1.imageType?.rawValue ?? "" } - for try await (imageInfo, URL) in group { - await MainActor.run { - self.images[imageInfo] = URL - } - } + self.images = IdentifiedArray(uniqueElements: updatedImages) } } @@ -435,30 +405,25 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } await MainActor.run { - self.images.removeValue(forKey: imageInfo) - - let updatedImages = self.images - .sorted { lhs, rhs in - guard let lhsType = lhs.key.imageType, let rhsType = rhs.key.imageType else { - return false - } - if lhsType != rhsType { - return lhsType.rawValue < rhsType.rawValue - } - return (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) - } - .reduce(into: [ImageInfo: URL]()) { result, pair in - var updatedInfo = pair.key - if updatedInfo.imageType == imageInfo.imageType, - let index = updatedInfo.imageIndex, - index > imageInfo.imageIndex! - { - updatedInfo.imageIndex = index - 1 + /// Remove the ImageInfo from local storage + self.images.removeAll { $0 == imageInfo } + + /// Ensure that the remaining items have the correct new indexes + if imageInfo.imageIndex != nil { + self.images = IdentifiedArray( + uniqueElements: self.images.map { image in + var updatedImage = image + if updatedImage.imageType == imageInfo.imageType, + let imageIndex = updatedImage.imageIndex, + let targetIndex = imageInfo.imageIndex, + imageIndex > targetIndex + { + updatedImage.imageIndex = imageIndex - 1 + } + return updatedImage } - result[updatedInfo] = pair.value - } - - self.images = updatedImages + ) + } } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 9df6e8610..54ce057c4 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -190,6 +190,8 @@ 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; }; 4EA78B252D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; 4EA78B262D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; + 4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; + 4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; 4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; }; 4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; }; @@ -1348,6 +1350,7 @@ 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoPickerView.swift; sourceTree = ""; }; 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCropView.swift; sourceTree = ""; }; 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCoordinator.swift; sourceTree = ""; }; + 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageType.swift; sourceTree = ""; }; 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = ""; }; 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = ""; }; 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = ""; }; @@ -4597,6 +4600,7 @@ 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */, E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */, 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */, + 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */, E1D842902933F87500D1041A /* ItemFields.swift */, E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, @@ -5780,6 +5784,7 @@ E1A1528E28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */, E18E021D2887492B0022598C /* AttributeStyleModifier.swift in Sources */, E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */, + 4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */, E10231492BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */, E1CB75702C80E66700217C76 /* CommaStringBuilder.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */, @@ -6240,6 +6245,7 @@ E1A1528D28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */, 4E35CE642CBED69600DBD886 /* TaskTriggerType.swift in Sources */, E18E01EE288747230022598C /* AboutView.swift in Sources */, + 4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */, 62E632E0267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */, E1B33EB028EA890D0073B0FD /* Equatable.swift in Sources */, E1549662296CA2EF00C4EF88 /* UserSession.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift index ce78bbb93..11674cf9d 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift @@ -13,7 +13,6 @@ extension ItemImageDetailsView { struct DetailsSection: View { - let imageID: Int? let imageURL: URL? let imageIndex: Int? let imageLanguage: String? @@ -27,7 +26,6 @@ extension ItemImageDetailsView { // MARK: - Initializer init( - imageID: Int? = nil, imageURL: URL? = nil, imageIndex: Int? = nil, imageLanguage: String? = nil, @@ -38,7 +36,6 @@ extension ItemImageDetailsView { ratingType: RatingType? = nil, ratingVotes: Int? = nil ) { - self.imageID = imageID self.imageURL = imageURL self.imageIndex = imageIndex self.imageLanguage = imageLanguage @@ -59,14 +56,6 @@ extension ItemImageDetailsView { TextPairView(leading: L10n.provider, trailing: provider) } - if let imageID { - TextPairView(leading: L10n.id, trailing: imageID.description) - } - - if let imageIndex { - TextPairView(leading: L10n.index, trailing: imageIndex.description) - } - if let imageLanguage { TextPairView(leading: L10n.language, trailing: imageLanguage) } @@ -77,6 +66,10 @@ extension ItemImageDetailsView { trailing: "\(imageWidth) x \(imageHeight)" ) } + + if let imageIndex { + TextPairView(leading: L10n.index, trailing: imageIndex.description) + } } if let rating { diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift index ccf43d699..15d055bb0 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift @@ -16,16 +16,15 @@ extension ItemImageDetailsView { // MARK: - Image Info - let imageURL: URL? - - let imageType: PosterDisplayType + let imageSource: ImageSource + let posterType: PosterDisplayType // MARK: - Header @ViewBuilder var body: some View { Section { - ImageView(imageURL) + ImageView(imageSource) .placeholder { _ in Image(systemName: "circle") } @@ -34,7 +33,7 @@ extension ItemImageDetailsView { } } .scaledToFit() - .posterStyle(imageType) + .posterStyle(posterType) .frame(maxWidth: .infinity) .listRowBackground(Color.clear) .listRowCornerRadius(0) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index 280937fc4..16b3fb1ae 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -10,6 +10,7 @@ import BlurHashKit import CollectionVGrid import Combine import Defaults +import Factory import JellyfinAPI import Nuke import SwiftUI @@ -21,6 +22,11 @@ struct ItemImageDetailsView: View { @Default(.accentColor) private var accentColor + // MARK: - User Session + + @Injected(\.currentUserSession) + private var userSession + // MARK: - State, Observed, & Environment Objects @EnvironmentObject @@ -31,7 +37,7 @@ struct ItemImageDetailsView: View { // MARK: - Image Variable - private let localImageInfo: (key: ImageInfo, value: URL)? + private let localImageInfo: ImageInfo? private let remoteImageInfo: RemoteImageInfo? @@ -47,7 +53,11 @@ struct ItemImageDetailsView: View { // MARK: - Initializer - init(viewModel: ItemImagesViewModel, localImageInfo: (key: ImageInfo, value: URL)? = nil, remoteImageInfo: RemoteImageInfo? = nil) { + init( + viewModel: ItemImagesViewModel, + localImageInfo: ImageInfo? = nil, + remoteImageInfo: RemoteImageInfo? = nil + ) { self._viewModel = ObservedObject(wrappedValue: viewModel) self.localImageInfo = localImageInfo self.remoteImageInfo = remoteImageInfo @@ -58,7 +68,7 @@ struct ItemImageDetailsView: View { var body: some View { contentView .navigationBarTitle( - localImageInfo?.key.imageType?.rawValue.localizedCapitalized ?? + localImageInfo?.imageType?.rawValue.localizedCapitalized ?? remoteImageInfo?.type?.rawValue.localizedCapitalized ?? "" ) @@ -89,7 +99,7 @@ struct ItemImageDetailsView: View { @ViewBuilder var contentView: some View { if let imageInfo = localImageInfo { - localImageView(imageInfo: imageInfo.key, imageURL: imageInfo.value) + localImageView(imageInfo: imageInfo) } else if let imageInfo = remoteImageInfo { remoteImageView(imageInfo: imageInfo) } else { @@ -98,15 +108,19 @@ struct ItemImageDetailsView: View { } @ViewBuilder - func localImageView(imageInfo: ImageInfo, imageURL: URL) -> some View { + func localImageView(imageInfo: ImageInfo) -> some View { + let imageSource = imageInfo.itemImageSource( + itemID: viewModel.item.id!, + client: userSession!.client + ) + List { HeaderSection( - imageURL: imageURL, - imageType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape + imageSource: imageSource, + posterType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape ) DetailsSection( - imageID: imageInfo.id, - imageURL: imageURL, + imageURL: imageSource.url, imageIndex: imageInfo.imageIndex, imageWidth: imageInfo.width, imageHeight: imageInfo.height @@ -120,14 +134,14 @@ struct ItemImageDetailsView: View { @ViewBuilder func remoteImageView(imageInfo: RemoteImageInfo) -> some View { let imageURL = URL(string: imageInfo.url) + let imageSource = ImageSource(url: imageURL) List { HeaderSection( - imageURL: URL(string: imageInfo.url), - imageType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape + imageSource: imageSource, + posterType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape ) DetailsSection( - imageID: imageInfo.id, imageURL: imageURL, imageLanguage: imageInfo.language, imageWidth: imageInfo.width, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 1c7d4c48c..301228376 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -10,6 +10,7 @@ import BlurHashKit import CollectionVGrid import Combine import Defaults +import Factory import JellyfinAPI import SwiftUI @@ -20,6 +21,11 @@ struct ItemImagesView: View { @Default(.accentColor) private var accentColor + // MARK: - User Session + + @Injected(\.currentUserSession) + private var userSession + // MARK: - Observed & Environment Objects @EnvironmentObject @@ -92,6 +98,25 @@ struct ItemImagesView: View { // MARK: - Content View private var contentView: some View { + ZStack { + switch viewModel.state { + case .content, .deleting, .updating: + imageView + case .initial: + DelayedProgressView() + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } + } + } + } + + // MARK: - Image View + + @ViewBuilder + private var imageView: some View { ScrollView { ForEach(orderedItems, id: \.self) { imageType in Section { @@ -104,23 +129,18 @@ struct ItemImagesView: View { } } - // MARK: - Image Scrolle View + // MARK: - Image Scroll View @ViewBuilder private func imageScrollView(for imageType: ImageType) -> some View { - let filteredImages = viewModel.images.filter { $0.key.imageType == imageType } - let imageArray = Array(filteredImages) + let imageArray = viewModel.images.filter { $0.imageType == imageType } if imageArray.isNotEmpty { - let sortedImageArray = imageArray.sorted { lhs, rhs in - (lhs.key.imageIndex ?? 0) < (rhs.key.imageIndex ?? 0) - } - ScrollView(.horizontal, showsIndicators: false) { HStack { - ForEach(sortedImageArray, id: \.key) { imageData in - imageButton(imageInfo: imageData.key, imageURL: imageData.value) { - router.route(to: \.deleteImage, imageData) + ForEach(imageArray, id: \.self) { imageInfo in + imageButton(imageInfo: imageInfo) { + router.route(to: \.deleteImage, imageInfo) } } } @@ -167,19 +187,27 @@ struct ItemImagesView: View { // MARK: - Image Button - private func imageButton(imageInfo: ImageInfo, imageURL: URL?, onSelect: @escaping () -> Void) -> some View { + private func imageButton( + imageInfo: ImageInfo, + onSelect: @escaping () -> Void + ) -> some View { Button(action: onSelect) { ZStack { Color.secondarySystemFill - ImageView(imageURL) - .placeholder { _ in - Image(systemName: "circle") - } - .failure { - Image(systemName: "circle") - } - .scaledToFit() - .frame(maxWidth: .infinity) + ImageView( + imageInfo.itemImageSource( + itemID: viewModel.item.id!, + client: userSession!.client + ) + ) + .placeholder { _ in + Image(systemName: "circle") + } + .failure { + Image(systemName: "circle") + } + .scaledToFit() + .frame(maxWidth: .infinity) } .scaledToFit() .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index db9ec98df..63d469db7 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -157,6 +157,9 @@ /// Arranger "arranger" = "Arranger"; +/// Art +"art" = "Art"; + /// Artist "artist" = "Artist"; @@ -211,6 +214,12 @@ /// Back "back" = "Back"; +/// Backdrop +"backdrop" = "Backdrop"; + +/// Banner +"banner" = "Banner"; + /// Bar Buttons "barButtons" = "Bar Buttons"; @@ -307,6 +316,12 @@ /// Books "books" = "Books"; +/// Box +"box" = "Box"; + +/// BoxRear +"boxRear" = "BoxRear"; + /// Bugs and Features "bugsAndFeatures" = "Bugs and Features"; @@ -337,6 +352,9 @@ /// Channels "channels" = "Channels"; +/// Chapter +"chapter" = "Chapter"; + /// Chapters "chapters" = "Chapters"; @@ -679,6 +697,9 @@ /// Disabled "disabled" = "Disabled"; +/// Disc +"disc" = "Disc"; + /// Disclaimer "disclaimer" = "Disclaimer"; @@ -1030,6 +1051,9 @@ /// Locked users "lockedUsers" = "Locked users"; +/// Logo +"logo" = "Logo"; + /// Logs "logs" = "Logs"; @@ -1090,6 +1114,9 @@ /// Mbps "megabitsPerSecond" = "Mbps"; +/// Menu +"menu" = "Menu"; + /// Menu Buttons "menuButtons" = "Menu Buttons"; @@ -1339,6 +1366,9 @@ /// Production Year "productionYear" = "Production Year"; +/// Profile +"profile" = "Profile"; + /// Profile Image "profileImage" = "Profile Image"; @@ -1546,6 +1576,9 @@ /// Score "score" = "Score"; +/// Screenshot +"screenshot" = "Screenshot"; + /// Scrub Current Time "scrubCurrentTime" = "Scrub Current Time"; @@ -1810,6 +1843,9 @@ /// Test Size "testSize" = "Test Size"; +/// Thumb +"thumb" = "Thumb"; + /// Time "time" = "Time"; From 6dfecf76f9b12748c7c255376fc6685a878e794b Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 6 Jan 2025 23:42:05 -0700 Subject: [PATCH 33/45] Remove attempt at ImageInfo Poster Comformance. --- Shared/Extensions/JellyfinAPI/ImageInfo.swift | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/Shared/Extensions/JellyfinAPI/ImageInfo.swift b/Shared/Extensions/JellyfinAPI/ImageInfo.swift index 62d2c79f7..f8cfd6c10 100644 --- a/Shared/Extensions/JellyfinAPI/ImageInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ImageInfo.swift @@ -9,29 +9,11 @@ import Foundation import JellyfinAPI -extension ImageInfo: @retroactive Identifiable, Poster { +extension ImageInfo: @retroactive Identifiable { public var id: Int { hashValue } - - // TODO: Remove if not using PosterHStack -> - var unwrappedIDHashOrZero: Int { - id - } - - var displayTitle: String { - if let imageIndex { - "\(imageType?.displayTitle ?? L10n.unknown)-\(imageIndex)" - } else { - imageType?.displayTitle ?? L10n.unknown - } - } - - var systemImage: String { - "circle" - } - // TODO: <- Remove if not using PosterHStack } extension ImageInfo { From 57fc8254bbf02f36032da85f47d5b605d476d9dc Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 10:01:46 -0700 Subject: [PATCH 34/45] Even more cleanup --- .../Coordinators/ItemImagesCoordinator.swift | 35 ++++- .../ItemImagesViewModel.swift | 11 +- .../UserProfileImageViewModel.swift | 2 +- .../AddItemImageView/AddItemImageView.swift | 5 +- .../ItemImageDetailsDetailsSection.swift | 46 +++--- .../ItemImageDetailsHeaderSection.swift | 2 +- .../ItemImageDetailsView.swift | 132 +++++++++--------- .../Components/PhotoCropView.swift | 4 +- 8 files changed, 129 insertions(+), 108 deletions(-) diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index 8b34f44c9..39edaf739 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -6,12 +6,16 @@ // Copyright (c) 2025 Jellyfin & Jellyfin Contributors // +import Factory import JellyfinAPI import Stinsen import SwiftUI final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { + @Injected(\.currentUserSession) + private var userSession + let stack = NavigationStack(initial: \ItemImagesCoordinator.start) @Root @@ -46,13 +50,40 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { func makeDeleteImage(imageInfo: ImageInfo) -> NavigationViewCoordinator { NavigationViewCoordinator { - ItemImageDetailsView(viewModel: self.viewModel, localImageInfo: imageInfo) + ItemImageDetailsView( + title: imageInfo.imageType?.displayTitle ?? "", + viewModel: self.viewModel, + imageSource: imageInfo.itemImageSource( + itemID: self.viewModel.item.id!, + client: self.userSession!.client + ), + index: imageInfo.imageIndex, + width: imageInfo.width, + height: imageInfo.height, + isLocal: false, onDelete: { + self.viewModel.send(.deleteImage(imageInfo)) + } + ) } } func makeSelectImage(remoteImageInfo: RemoteImageInfo) -> NavigationViewCoordinator { NavigationViewCoordinator { - ItemImageDetailsView(viewModel: self.viewModel, remoteImageInfo: remoteImageInfo) + ItemImageDetailsView( + title: remoteImageInfo.type?.displayTitle ?? "", + viewModel: self.viewModel, + imageSource: ImageSource(url: URL(string: remoteImageInfo.url)), + width: remoteImageInfo.width, + height: remoteImageInfo.height, + language: remoteImageInfo.language, + provider: remoteImageInfo.providerName, + rating: remoteImageInfo.communityRating, + ratingType: remoteImageInfo.ratingType, + ratingVotes: remoteImageInfo.voteCount, + isLocal: false, onSave: { + self.viewModel.send(.setImage(remoteImageInfo)) + } + ) } } diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 7e777c0bc..8c4d79727 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -43,10 +43,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { case error(JellyfinAPIError) } - // MARK: - Image Variables - - private let includeAllLanguages: Bool - // MARK: - Published Variables @Published @@ -72,13 +68,8 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { // MARK: - Init - init( - item: BaseItemDto, - includeAllLanguages: Bool = false - ) { + init(item: BaseItemDto) { self.item = item - self.includeAllLanguages = includeAllLanguages - super.init() } // MARK: - Respond to Actions diff --git a/Shared/ViewModels/UserProfileImageViewModel.swift b/Shared/ViewModels/UserProfileImageViewModel.swift index adea383ac..bcdff63e1 100644 --- a/Shared/ViewModels/UserProfileImageViewModel.swift +++ b/Shared/ViewModels/UserProfileImageViewModel.swift @@ -151,7 +151,7 @@ class UserProfileImageViewModel: ViewModel, Eventful, Stateful { ) request.headers = ["Content-Type": contentType] - guard imageData.count < 30_000_000 else { + guard imageData.count <= 30_000_000 else { throw JellyfinAPIError( "This profile image is too large (\(imageData.count.formatted(.byteCount(style: .file)))). The upload limit for images is 30 MB." ) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index c9b439cdd..643e93b1c 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -159,10 +159,7 @@ struct AddItemImageView: View { } } .failure { - VStack(spacing: 8) { - Image(systemName: "photo") - Text(L10n.none) - } + Image(systemName: "questionmark") } .foregroundColor(.secondary) .font(.headline) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift index 11674cf9d..eeb9a8912 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift @@ -13,11 +13,11 @@ extension ItemImageDetailsView { struct DetailsSection: View { - let imageURL: URL? - let imageIndex: Int? - let imageLanguage: String? - let imageWidth: Int? - let imageHeight: Int? + let url: URL? + let index: Int? + let language: String? + let width: Int? + let height: Int? let provider: String? let rating: Double? let ratingType: RatingType? @@ -26,21 +26,21 @@ extension ItemImageDetailsView { // MARK: - Initializer init( - imageURL: URL? = nil, - imageIndex: Int? = nil, - imageLanguage: String? = nil, - imageWidth: Int? = nil, - imageHeight: Int? = nil, + url: URL? = nil, + index: Int? = nil, + language: String? = nil, + width: Int? = nil, + height: Int? = nil, provider: String? = nil, rating: Double? = nil, ratingType: RatingType? = nil, ratingVotes: Int? = nil ) { - self.imageURL = imageURL - self.imageIndex = imageIndex - self.imageLanguage = imageLanguage - self.imageWidth = imageWidth - self.imageHeight = imageHeight + self.url = url + self.index = index + self.language = language + self.width = width + self.height = height self.provider = provider self.rating = rating self.ratingType = ratingType @@ -56,19 +56,19 @@ extension ItemImageDetailsView { TextPairView(leading: L10n.provider, trailing: provider) } - if let imageLanguage { - TextPairView(leading: L10n.language, trailing: imageLanguage) + if let language { + TextPairView(leading: L10n.language, trailing: language) } - if let imageWidth, let imageHeight { + if let width, let height { TextPairView( leading: L10n.dimensions, - trailing: "\(imageWidth) x \(imageHeight)" + trailing: "\(width) x \(height)" ) } - if let imageIndex { - TextPairView(leading: L10n.index, trailing: imageIndex.description) + if let index { + TextPairView(leading: L10n.index, trailing: index.description) } } @@ -86,10 +86,10 @@ extension ItemImageDetailsView { } } - if let imageURL = imageURL { + if let url { Section { Button { - UIApplication.shared.open(imageURL) + UIApplication.shared.open(url) } label: { HStack { Text(L10n.imageSource) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift index 15d055bb0..77c720920 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift @@ -19,7 +19,7 @@ extension ItemImageDetailsView { let imageSource: ImageSource let posterType: PosterDisplayType - // MARK: - Header + // MARK: - Body @ViewBuilder var body: some View { diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index 16b3fb1ae..5b46c70c9 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -22,24 +22,38 @@ struct ItemImageDetailsView: View { @Default(.accentColor) private var accentColor - // MARK: - User Session - - @Injected(\.currentUserSession) - private var userSession - // MARK: - State, Observed, & Environment Objects @EnvironmentObject private var router: BasicNavigationViewCoordinator.Router @ObservedObject - var viewModel: ItemImagesViewModel + private var viewModel: ItemImagesViewModel // MARK: - Image Variable - private let localImageInfo: ImageInfo? + private let imageSource: ImageSource + + // MARK: - Navigation Title + + private let title: String + + // MARK: - Description Variables + + private let index: Int? + private let width: Int? + private let height: Int? + private let language: String? + private let provider: String? + private let rating: Double? + private let ratingType: RatingType? + private let ratingVotes: Int? + private let isLocal: Bool + + // MARK: - Image Actions - private let remoteImageInfo: RemoteImageInfo? + private let onSave: (() -> Void)? + private let onDelete: (() -> Void)? // MARK: - Dialog States @@ -54,24 +68,42 @@ struct ItemImageDetailsView: View { // MARK: - Initializer init( + title: String, viewModel: ItemImagesViewModel, - localImageInfo: ImageInfo? = nil, - remoteImageInfo: RemoteImageInfo? = nil + imageSource: ImageSource, + index: Int? = nil, + width: Int? = nil, + height: Int? = nil, + language: String? = nil, + provider: String? = nil, + rating: Double? = nil, + ratingType: RatingType? = nil, + ratingVotes: Int? = nil, + isLocal: Bool, + onSave: (() -> Void)? = nil, + onDelete: (() -> Void)? = nil ) { + self.title = title self._viewModel = ObservedObject(wrappedValue: viewModel) - self.localImageInfo = localImageInfo - self.remoteImageInfo = remoteImageInfo + self.imageSource = imageSource + self.index = index + self.width = width + self.height = height + self.language = language + self.provider = provider + self.rating = rating + self.ratingType = ratingType + self.ratingVotes = ratingVotes + self.isLocal = isLocal + self.onSave = onSave + self.onDelete = onDelete } // MARK: - Body var body: some View { contentView - .navigationBarTitle( - localImageInfo?.imageType?.rawValue.localizedCapitalized ?? - remoteImageInfo?.type?.rawValue.localizedCapitalized ?? - "" - ) + .navigationBarTitle(title.localizedCapitalized) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { router.dismissCoordinator() @@ -98,65 +130,35 @@ struct ItemImageDetailsView: View { @ViewBuilder var contentView: some View { - if let imageInfo = localImageInfo { - localImageView(imageInfo: imageInfo) - } else if let imageInfo = remoteImageInfo { - remoteImageView(imageInfo: imageInfo) - } else { - ErrorView(error: JellyfinAPIError("No image provided.")) - } - } - - @ViewBuilder - func localImageView(imageInfo: ImageInfo) -> some View { - let imageSource = imageInfo.itemImageSource( - itemID: viewModel.item.id!, - client: userSession!.client - ) - List { HeaderSection( imageSource: imageSource, - posterType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape + posterType: height ?? 0 > width ?? 0 ? .portrait : .landscape ) + DetailsSection( - imageURL: imageSource.url, - imageIndex: imageInfo.imageIndex, - imageWidth: imageInfo.width, - imageHeight: imageInfo.height + url: imageSource.url, + index: index, + language: language, + width: width, + height: height, + provider: provider, + rating: rating, + ratingType: ratingType, + ratingVotes: ratingVotes ) + DeleteButton { - viewModel.send(.deleteImage(imageInfo)) + if !isLocal, let onDelete { + onDelete() + } } } - } - - @ViewBuilder - func remoteImageView(imageInfo: RemoteImageInfo) -> some View { - let imageURL = URL(string: imageInfo.url) - let imageSource = ImageSource(url: imageURL) - - List { - HeaderSection( - imageSource: imageSource, - posterType: imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape - ) - DetailsSection( - imageURL: imageURL, - imageLanguage: imageInfo.language, - imageWidth: imageInfo.width, - imageHeight: imageInfo.height, - provider: imageInfo.providerName, - rating: imageInfo.communityRating, - ratingType: imageInfo.ratingType, - ratingVotes: imageInfo.voteCount - ) - } .topBarTrailing { - Button(L10n.save) { - viewModel.send(.setImage(imageInfo)) + if isLocal, let onSave { + Button(L10n.save, action: onSave) + .buttonStyle(.toolbarPill) } - .buttonStyle(.toolbarPill) } } } diff --git a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift index 7c1142c6c..7819305a1 100644 --- a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift +++ b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift @@ -24,8 +24,8 @@ struct PhotoCropView: View { // MARK: - Image Variable - var isReady: Bool - var isSaving: Bool + let isReady: Bool + let isSaving: Bool let image: UIImage let cropShape: Mantis.CropShapeType let presetRatio: Mantis.PresetFixedRatioType From 710e9a67d9753238b05f58a62ccf639fcc67542b Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 10:02:51 -0700 Subject: [PATCH 35/45] Delete vs Save flip --- Shared/Coordinators/ItemImagesCoordinator.swift | 6 ++++-- .../ItemImageDetailsView/ItemImageDetailsView.swift | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index 39edaf739..43403666f 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -60,7 +60,8 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { index: imageInfo.imageIndex, width: imageInfo.width, height: imageInfo.height, - isLocal: false, onDelete: { + isLocal: true, + onDelete: { self.viewModel.send(.deleteImage(imageInfo)) } ) @@ -80,7 +81,8 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { rating: remoteImageInfo.communityRating, ratingType: remoteImageInfo.ratingType, ratingVotes: remoteImageInfo.voteCount, - isLocal: false, onSave: { + isLocal: false, + onSave: { self.viewModel.send(.setImage(remoteImageInfo)) } ) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index 5b46c70c9..fd3e702d1 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -149,13 +149,13 @@ struct ItemImageDetailsView: View { ) DeleteButton { - if !isLocal, let onDelete { + if isLocal, let onDelete { onDelete() } } } .topBarTrailing { - if isLocal, let onSave { + if !isLocal, let onSave { Button(L10n.save, action: onSave) .buttonStyle(.toolbarPill) } From 7a7c875a17fe29b36d2afc59334b00121275145c Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 10:05:10 -0700 Subject: [PATCH 36/45] Hide delete button --- .../ItemImageDetailsView/ItemImageDetailsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index fd3e702d1..d4c525c26 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -148,8 +148,8 @@ struct ItemImageDetailsView: View { ratingVotes: ratingVotes ) - DeleteButton { - if isLocal, let onDelete { + if isLocal, let onDelete { + DeleteButton { onDelete() } } From 57eab13b57de6835468d5e5003d42aa3c10e217a Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 11:01:40 -0700 Subject: [PATCH 37/45] Even more cleanup --- ...swift => ItemImagePickerCoordinator.swift} | 18 ++--- .../Coordinators/ItemImagesCoordinator.swift | 66 +++++++++++-------- .../UserProfileImageCoordinator.swift | 10 +-- .../ItemImagesViewModel.swift | 32 +-------- Swiftfin.xcodeproj/project.pbxproj | 14 ++-- .../AddItemImageView/AddItemImageView.swift | 35 +++++----- .../ItemImageDetailsDeleteButton.swift | 2 +- .../ItemImageDetailsDetailsSection.swift | 10 ++- .../ItemImageDetailsHeaderSection.swift | 1 + .../ItemImageDetailsView.swift | 5 +- .../ItemImagesView/ItemImagesView.swift | 2 +- .../Components/ItemPhotoCropView.swift | 3 +- .../ItemPhotoPickerView.swift | 2 +- 13 files changed, 89 insertions(+), 111 deletions(-) rename Shared/Coordinators/{ItemPhotoCoordinator.swift => ItemImagePickerCoordinator.swift} (70%) diff --git a/Shared/Coordinators/ItemPhotoCoordinator.swift b/Shared/Coordinators/ItemImagePickerCoordinator.swift similarity index 70% rename from Shared/Coordinators/ItemPhotoCoordinator.swift rename to Shared/Coordinators/ItemImagePickerCoordinator.swift index 96d925c31..d63e54558 100644 --- a/Shared/Coordinators/ItemPhotoCoordinator.swift +++ b/Shared/Coordinators/ItemImagePickerCoordinator.swift @@ -10,11 +10,11 @@ import JellyfinAPI import Stinsen import SwiftUI -final class ItemPhotoCoordinator: NavigationCoordinatable { +final class ItemImagePickerCoordinator: NavigationCoordinatable { - // MARK: - Navigation Components + // MARK: - Navigation Stack - let stack = Stinsen.NavigationStack(initial: \ItemPhotoCoordinator.start) + let stack = Stinsen.NavigationStack(initial: \ItemImagePickerCoordinator.start) @Root var start = makeStart @@ -29,6 +29,8 @@ final class ItemPhotoCoordinator: NavigationCoordinatable { @ObservedObject var viewModel: ItemImagesViewModel + // MARK: - Image Variable + let type: ImageType // MARK: - Initializer @@ -38,22 +40,14 @@ final class ItemPhotoCoordinator: NavigationCoordinatable { self.type = type } - // MARK: - Views + // MARK: - Crop Image View func makeCropImage(image: UIImage) -> some View { - #if os(iOS) ItemPhotoCropView(viewModel: viewModel, image: image, type: type) - #else - AssertionFailureView("not implemented") - #endif } @ViewBuilder func makeStart() -> some View { - #if os(iOS) ItemImagePicker() - #else - AssertionFailureView("not implemented") - #endif } } diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index 43403666f..6ee2b98d4 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -13,9 +13,13 @@ import SwiftUI final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { + // MARK: - User Session + @Injected(\.currentUserSession) private var userSession + // MARK: - Navigation Stack + let stack = NavigationStack(initial: \ItemImagesCoordinator.start) @Root @@ -24,14 +28,20 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { @ObservedObject private var viewModel: ItemImagesViewModel - // MARK: - Route to Views + // MARK: - Route to Delete Local Image - @Route(.push) - var addImage = makeAddImage @Route(.modal) var deleteImage = makeDeleteImage + + // MARK: - Route to Add Remote Image + + @Route(.push) + var addImage = makeAddImage @Route(.modal) var selectImage = makeSelectImage + + // MARK: - Route to Photo Picker + @Route(.modal) var photoPicker = makePhotoPicker @@ -41,33 +51,13 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { self._viewModel = ObservedObject(wrappedValue: viewModel) } - // MARK: - Item Images + // MARK: - Add Remote Images View @ViewBuilder func makeAddImage(imageType: ImageType) -> some View { AddItemImageView(viewModel: viewModel, imageType: imageType) } - func makeDeleteImage(imageInfo: ImageInfo) -> NavigationViewCoordinator { - NavigationViewCoordinator { - ItemImageDetailsView( - title: imageInfo.imageType?.displayTitle ?? "", - viewModel: self.viewModel, - imageSource: imageInfo.itemImageSource( - itemID: self.viewModel.item.id!, - client: self.userSession!.client - ), - index: imageInfo.imageIndex, - width: imageInfo.width, - height: imageInfo.height, - isLocal: true, - onDelete: { - self.viewModel.send(.deleteImage(imageInfo)) - } - ) - } - } - func makeSelectImage(remoteImageInfo: RemoteImageInfo) -> NavigationViewCoordinator { NavigationViewCoordinator { ItemImageDetailsView( @@ -89,8 +79,32 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { } } - func makePhotoPicker(type: ImageType) -> NavigationViewCoordinator { - NavigationViewCoordinator(ItemPhotoCoordinator(viewModel: self.viewModel, type: type)) + // MARK: - Delete Local Image View + + func makeDeleteImage(imageInfo: ImageInfo) -> NavigationViewCoordinator { + NavigationViewCoordinator { + ItemImageDetailsView( + title: imageInfo.imageType?.displayTitle ?? "", + viewModel: self.viewModel, + imageSource: imageInfo.itemImageSource( + itemID: self.viewModel.item.id!, + client: self.userSession!.client + ), + index: imageInfo.imageIndex, + width: imageInfo.width, + height: imageInfo.height, + isLocal: true, + onDelete: { + self.viewModel.send(.deleteImage(imageInfo)) + } + ) + } + } + + // MARK: - Photo Picker View + + func makePhotoPicker(type: ImageType) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemImagePickerCoordinator(viewModel: self.viewModel, type: type)) } // MARK: - Start diff --git a/Shared/Coordinators/UserProfileImageCoordinator.swift b/Shared/Coordinators/UserProfileImageCoordinator.swift index 520d23ab3..e5188896e 100644 --- a/Shared/Coordinators/UserProfileImageCoordinator.swift +++ b/Shared/Coordinators/UserProfileImageCoordinator.swift @@ -11,7 +11,7 @@ import SwiftUI final class UserProfileImageCoordinator: NavigationCoordinatable { - // MARK: - Navigation Components + // MARK: - Navigation Stack let stack = Stinsen.NavigationStack(initial: \UserProfileImageCoordinator.start) @@ -37,19 +37,11 @@ final class UserProfileImageCoordinator: NavigationCoordinatable { // MARK: - Views func makeCropImage(image: UIImage) -> some View { - #if os(iOS) UserProfileImageCropView(viewModel: viewModel, image: image) - #else - AssertionFailureView("not implemented") - #endif } @ViewBuilder func makeStart() -> some View { - #if os(iOS) UserProfileImagePickerView() - #else - AssertionFailureView("not implemented") - #endif } } diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 8c4d79727..0a5594a71 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -24,7 +24,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { enum Action: Equatable { case cancel case refresh - case backgroundRefresh case setImage(RemoteImageInfo) case uploadPhoto(image: UIImage, type: ImageType) case uploadImage(file: URL, type: ImageType) @@ -83,32 +82,6 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { return state - case .backgroundRefresh: - task?.cancel() - - task = Task { [weak self] in - guard let self else { return } - do { - await MainActor.run { - _ = self.backgroundStates.append(.refreshing) - } - - try await self.getAllImages() - - await MainActor.run { - _ = self.backgroundStates.remove(.refreshing) - } - } catch { - let apiError = JellyfinAPIError(error.localizedDescription) - await MainActor.run { - self.eventSubject.send(.error(apiError)) - _ = self.backgroundStates.remove(.refreshing) - } - } - }.asAnyCancellable() - - return state - case .refresh: task?.cancel() @@ -150,6 +123,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } try await self.setImage(remoteImageInfo) + try await self.getAllImages() await MainActor.run { self.eventSubject.send(.updated) @@ -177,7 +151,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } try await self.uploadPhoto(image, type: type) - try await self.refreshItem() + try await self.getAllImages() await MainActor.run { self.state = .content @@ -205,7 +179,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } try await self.uploadImage(url, type: type) - try await self.refreshItem() + try await self.getAllImages() await MainActor.run { self.state = .content diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 54ce057c4..df72e26f8 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -188,8 +188,8 @@ 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */; }; 4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */; }; 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; }; - 4EA78B252D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; - 4EA78B262D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */; }; + 4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */; }; + 4EA78B262D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */; }; 4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; 4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; @@ -544,7 +544,6 @@ E11BDF972B865F550045C54A /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF962B865F550045C54A /* ItemTag.swift */; }; E11BDF982B865F550045C54A /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF962B865F550045C54A /* ItemTag.swift */; }; E11C15352BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */; }; - E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */; }; E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */; }; E11CEB8B28998552003E74C7 /* View-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8A28998552003E74C7 /* View-iOS.swift */; }; E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8C28999B4A003E74C7 /* Font.swift */; }; @@ -1349,7 +1348,7 @@ 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsView.swift; sourceTree = ""; }; 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoPickerView.swift; sourceTree = ""; }; 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCropView.swift; sourceTree = ""; }; - 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCoordinator.swift; sourceTree = ""; }; + 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePickerCoordinator.swift; sourceTree = ""; }; 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageType.swift; sourceTree = ""; }; 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = ""; }; 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = ""; }; @@ -3566,8 +3565,8 @@ 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */, + 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */, 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */, - 4EA78B242D2B5DB20093BFCE /* ItemPhotoCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, E102312B2BCF8A08009D71FC /* LiveTVCoordinator */, C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */, @@ -5474,7 +5473,6 @@ E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E1575E95293E7B1E001665B1 /* Font.swift in Sources */, 4E49DED02CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */, - E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, E172D3AE2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, 4EC1C8532C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */, 4E24ECFC2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */, @@ -5766,7 +5764,7 @@ E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */, E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */, BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */, - 4EA78B262D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */, + 4EA78B262D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */, E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */, E1575E9A293E7B1E001665B1 /* Array.swift in Sources */, E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */, @@ -6254,7 +6252,7 @@ E10B1ECD2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */, E1401CA92938140700E8B599 /* DarkAppIcon.swift in Sources */, E1A1529028FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */, - 4EA78B252D2B5DBD0093BFCE /* ItemPhotoCoordinator.swift in Sources */, + 4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */, E11042752B8013DF00821020 /* Stateful.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 643e93b1c..374812f9f 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -9,17 +9,11 @@ import BlurHashKit import CollectionVGrid import Combine -import Defaults import JellyfinAPI import SwiftUI struct AddItemImageView: View { - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - // MARK: - Observed, & Environment Objects @EnvironmentObject @@ -68,6 +62,21 @@ struct AddItemImageView: View { .onFirstAppear { remoteImageInfoViewModel.send(.refresh) } + .onReceive(viewModel.events) { event in + switch event { + case .deleted: + break + case .updated: + UIDevice.feedback(.success) + // TODO: Why does this crash without the delay? + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + router.pop() + } + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + } + } .errorMessage($error) } @@ -110,20 +119,6 @@ struct AddItemImageView: 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) - } - } - // MARK: - Poster Image Button private func imageButton(_ image: RemoteImageInfo?) -> some View { diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift index 3538bbf9d..ca1b3bb01 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift @@ -28,7 +28,7 @@ extension ItemImageDetailsView { @State var isPresentingConfirmation: Bool = false - // MARK: - Header + // MARK: - Body @ViewBuilder var body: some View { diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift index eeb9a8912..5d72260a2 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift @@ -13,16 +13,24 @@ extension ItemImageDetailsView { struct DetailsSection: View { - let url: URL? + // MARK: - Image Details Variables + let index: Int? let language: String? let width: Int? let height: Int? let provider: String? + + // MARK: - Image Ratings Variables + let rating: Double? let ratingType: RatingType? let ratingVotes: Int? + // MARK: - Image Source Variable + + let url: URL? + // MARK: - Initializer init( diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift index 77c720920..73b92db4b 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift @@ -33,6 +33,7 @@ extension ItemImageDetailsView { } } .scaledToFit() + .frame(maxHeight: 300) .posterStyle(posterType) .frame(maxWidth: .infinity) .listRowBackground(Color.clear) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index d4c525c26..c325e263b 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -48,6 +48,9 @@ struct ItemImageDetailsView: View { private let rating: Double? private let ratingType: RatingType? private let ratingVotes: Int? + + // MARK: - RemoteImageInfo vs ImageInfo + private let isLocal: Bool // MARK: - Image Actions @@ -55,7 +58,7 @@ struct ItemImageDetailsView: View { private let onSave: (() -> Void)? private let onDelete: (() -> Void)? - // MARK: - Dialog States + // MARK: - Error State @State private var error: Error? diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 301228376..07ea3e763 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -85,7 +85,7 @@ struct ItemImagesView: View { .onReceive(viewModel.events) { event in switch event { case .updated: - viewModel.send(.refresh) + break // viewModel.send(.refresh) case .deleted: break case let .error(eventError): diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift index 019109591..c10da1e9a 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift @@ -21,7 +21,7 @@ struct ItemPhotoCropView: View { // MARK: - State, Observed, & Environment Objects @EnvironmentObject - private var router: ItemPhotoCoordinator.Router + private var router: ItemImagePickerCoordinator.Router @ObservedObject var viewModel: ItemImagesViewModel @@ -29,7 +29,6 @@ struct ItemPhotoCropView: View { // MARK: - Image Variable let image: UIImage - let type: ImageType // MARK: - Error State diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift index cbc27747e..aa1ac5fa1 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift @@ -14,7 +14,7 @@ struct ItemImagePicker: View { // MARK: - Observed, & Environment Objects @EnvironmentObject - private var router: ItemPhotoCoordinator.Router + private var router: ItemImagePickerCoordinator.Router // MARK: - Body From 938ad7a51844a79dcd2659c6f24877c32347e4e5 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 11:09:58 -0700 Subject: [PATCH 38/45] Fix tvOS build issues. --- Swiftfin.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index df72e26f8..31de5317d 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -189,7 +189,6 @@ 4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */; }; 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; }; 4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */; }; - 4EA78B262D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */; }; 4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; 4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; @@ -5764,7 +5763,6 @@ E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */, E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */, BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */, - 4EA78B262D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */, E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */, E1575E9A293E7B1E001665B1 /* Array.swift in Sources */, E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */, From 4ab813110d23cf3e5d5b048533cb2727dfd020ab Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 13:56:02 -0700 Subject: [PATCH 39/45] Reduce delay & remove unused comment. Should finally be ready again. --- .../ItemImages/AddItemImageView/AddItemImageView.swift | 2 +- .../ItemImages/ItemImagesView/ItemImagesView.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift index 374812f9f..9e5c15e09 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift @@ -69,7 +69,7 @@ struct AddItemImageView: View { case .updated: UIDevice.feedback(.success) // TODO: Why does this crash without the delay? - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { router.pop() } case let .error(eventError): diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift index 07ea3e763..084c6a795 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift @@ -85,7 +85,7 @@ struct ItemImagesView: View { .onReceive(viewModel.events) { event in switch event { case .updated: - break // viewModel.send(.refresh) + break case .deleted: break case let .error(eventError): @@ -204,7 +204,7 @@ struct ItemImagesView: View { Image(systemName: "circle") } .failure { - Image(systemName: "circle") + Image(systemName: "questionmark") } .scaledToFit() .frame(maxWidth: .infinity) From 87a5d001beb32402b47879cef73e70bf19acaaae Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 7 Jan 2025 15:48:30 -0700 Subject: [PATCH 40/45] wip --- .../Coordinators/ItemImagesCoordinator.swift | 15 +-- Swiftfin.xcodeproj/project.pbxproj | 20 +--- .../AddItemImageView.swift | 94 ++++++++---------- .../ItemImageDetailsDeleteButton.swift | 16 +-- .../ItemImageDetailsDetailsSection.swift | 17 +--- .../ItemImageDetailsView.swift | 39 ++------ .../{ItemImagesView => }/ItemImagesView.swift | 97 +++++++++---------- .../Components/ItemPhotoCropView.swift | 6 -- .../Components/PhotoCropView.swift | 41 ++------ .../PhotoPickerView/PhotoPickerView.swift | 11 +-- .../Components/UserProfileImageCropView.swift | 6 -- 11 files changed, 131 insertions(+), 231 deletions(-) rename Swiftfin/Views/ItemEditorView/ItemImages/{AddItemImageView => }/AddItemImageView.swift (60%) rename Swiftfin/Views/ItemEditorView/ItemImages/{ItemImagesView => }/ItemImagesView.swift (72%) diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index 6ee2b98d4..186e5a280 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -13,11 +13,6 @@ import SwiftUI final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { - // MARK: - User Session - - @Injected(\.currentUserSession) - private var userSession - // MARK: - Navigation Stack let stack = NavigationStack(initial: \ItemImagesCoordinator.start) @@ -25,6 +20,7 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { @Root var start = makeStart + // Okay for now since `ObservedObject` is on `MainActor` @ObservedObject private var viewModel: ItemImagesViewModel @@ -61,7 +57,6 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { func makeSelectImage(remoteImageInfo: RemoteImageInfo) -> NavigationViewCoordinator { NavigationViewCoordinator { ItemImageDetailsView( - title: remoteImageInfo.type?.displayTitle ?? "", viewModel: self.viewModel, imageSource: ImageSource(url: URL(string: remoteImageInfo.url)), width: remoteImageInfo.width, @@ -71,11 +66,11 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { rating: remoteImageInfo.communityRating, ratingType: remoteImageInfo.ratingType, ratingVotes: remoteImageInfo.voteCount, - isLocal: false, onSave: { self.viewModel.send(.setImage(remoteImageInfo)) } ) + .navigationTitle(remoteImageInfo.type?.displayTitle ?? "") } } @@ -84,20 +79,20 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { func makeDeleteImage(imageInfo: ImageInfo) -> NavigationViewCoordinator { NavigationViewCoordinator { ItemImageDetailsView( - title: imageInfo.imageType?.displayTitle ?? "", viewModel: self.viewModel, imageSource: imageInfo.itemImageSource( itemID: self.viewModel.item.id!, - client: self.userSession!.client + client: self.viewModel.userSession.client ), index: imageInfo.imageIndex, width: imageInfo.width, height: imageInfo.height, - isLocal: true, onDelete: { self.viewModel.send(.deleteImage(imageInfo)) } ) + .navigationTitle(imageInfo.imageType?.displayTitle ?? "") + .environment(\.isEditing, true) } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 31de5317d..2448d555b 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2311,9 +2311,9 @@ 4E37F6182D17EB220022AADD /* ItemImages */ = { isa = PBXGroup; children = ( - 4E4593A42D04E4D600E277E1 /* AddItemImageView */, + 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */, 4EA78B152D2A0C4A0093BFCE /* ItemImageDetailsView */, - 4E4593A12D04E2A200E277E1 /* ItemImagesView */, + 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */, 4EA78B1E2D2B5A960093BFCE /* ItemPhotoPickerView */, ); path = ItemImages; @@ -2338,22 +2338,6 @@ path = PlaybackBitrate; sourceTree = ""; }; - 4E4593A12D04E2A200E277E1 /* ItemImagesView */ = { - isa = PBXGroup; - children = ( - 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */, - ); - path = ItemImagesView; - sourceTree = ""; - }; - 4E4593A42D04E4D600E277E1 /* AddItemImageView */ = { - isa = PBXGroup; - children = ( - 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */, - ); - path = AddItemImageView; - sourceTree = ""; - }; 4E49DEDE2CE55F7F00352DCD /* Components */ = { isa = PBXGroup; children = ( diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift similarity index 60% rename from Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift rename to Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift index 9e5c15e09..f102c1b64 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift @@ -22,7 +22,7 @@ struct AddItemImageView: View { @ObservedObject private var viewModel: ItemImagesViewModel - @ObservedObject + @StateObject private var remoteImageInfoViewModel: RemoteImageInfoViewModel // MARK: - Dialog States @@ -38,63 +38,56 @@ struct AddItemImageView: View { // MARK: - Initializer init(viewModel: ItemImagesViewModel, imageType: ImageType) { - self._viewModel = ObservedObject(wrappedValue: viewModel) - self._remoteImageInfoViewModel = ObservedObject( - initialValue: RemoteImageInfoViewModel( - item: viewModel.item, - imageType: imageType - ) - ) + self.viewModel = viewModel + self._remoteImageInfoViewModel = StateObject(wrappedValue: RemoteImageInfoViewModel( + item: viewModel.item, + imageType: imageType + )) } // MARK: - Body var body: some View { - contentView - .navigationBarTitle(remoteImageInfoViewModel.imageType.rawValue.localizedCapitalized) - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(viewModel.state == .updating) - .topBarTrailing { - if viewModel.backgroundStates.contains(.refreshing) { - ProgressView() - } + ZStack { + switch remoteImageInfoViewModel.state { + case .initial, .refreshing: + DelayedProgressView() + case .content: + gridView + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } } - .onFirstAppear { - remoteImageInfoViewModel.send(.refresh) + } + .navigationTitle(remoteImageInfoViewModel.imageType.displayTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(viewModel.state == .updating) + .topBarTrailing { + if viewModel.backgroundStates.contains(.refreshing) { + ProgressView() } - .onReceive(viewModel.events) { event in - switch event { - case .deleted: - break - case .updated: - UIDevice.feedback(.success) - // TODO: Why does this crash without the delay? - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - router.pop() - } - case let .error(eventError): - UIDevice.feedback(.error) - error = eventError + } + .onFirstAppear { + remoteImageInfoViewModel.send(.refresh) + } + .onReceive(viewModel.events) { event in + switch event { + case .deleted: + break + case .updated: + UIDevice.feedback(.success) + // TODO: Why does this crash without the delay? + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + router.pop() } + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError } - .errorMessage($error) - } - - // MARK: - Content View - - @ViewBuilder - private var contentView: some View { - switch remoteImageInfoViewModel.state { - case .initial, .refreshing: - DelayedProgressView() - case .content: - gridView - case let .error(error): - ErrorView(error: error) - .onRetry { - viewModel.send(.refresh) - } } + .errorMessage($error) } // MARK: - Content Grid View @@ -103,9 +96,6 @@ struct AddItemImageView: View { private var gridView: some View { if remoteImageInfoViewModel.elements.isEmpty { Text(L10n.none) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .listRowSeparator(.hidden) - .listRowInsets(.zero) } else { CollectionVGrid( uniqueElements: remoteImageInfoViewModel.elements, @@ -121,6 +111,7 @@ struct AddItemImageView: View { // MARK: - Poster Image Button + @ViewBuilder private func imageButton(_ image: RemoteImageInfo?) -> some View { Button { if let image { @@ -136,6 +127,7 @@ struct AddItemImageView: View { // MARK: - Poster Image + @ViewBuilder private func posterImage( _ posterImageInfo: RemoteImageInfo?, posterStyle: PosterDisplayType diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift index ca1b3bb01..ef769b0df 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift @@ -26,7 +26,7 @@ extension ItemImageDetailsView { // MARK: - Dialog State @State - var isPresentingConfirmation: Bool = false + private var isPresentingConfirmation: Bool = false // MARK: - Body @@ -35,18 +35,18 @@ extension ItemImageDetailsView { ListRowButton(L10n.delete) { isPresentingConfirmation = true } - .foregroundStyle( - accentColor.overlayColor, - .red - ) + .foregroundStyle(.red, .red.opacity(0.2)) .confirmationDialog( L10n.delete, isPresented: $isPresentingConfirmation, titleVisibility: .visible ) { - Button(L10n.delete, role: .destructive) { - onDelete() - } + Button( + L10n.delete, + role: .destructive, + action: onDelete + ) + Button(L10n.cancel, role: .cancel) { isPresentingConfirmation = false } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift index 5d72260a2..5dffe280d 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift @@ -96,19 +96,12 @@ extension ItemImageDetailsView { if let url { Section { - Button { + ChevronButton( + L10n.imageSource, + external: true + ) + .onSelect { UIApplication.shared.open(url) - } label: { - HStack { - Text(L10n.imageSource) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundColor(.primary) - - Image(systemName: "arrow.up.forward") - .font(.body.weight(.regular)) - .foregroundColor(.secondary) - } - .foregroundStyle(.primary, .secondary) } } } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index c325e263b..b29671c41 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -17,10 +17,8 @@ import SwiftUI struct ItemImageDetailsView: View { - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor + @Environment(\.isEditing) + private var isEditing // MARK: - State, Observed, & Environment Objects @@ -34,10 +32,6 @@ struct ItemImageDetailsView: View { private let imageSource: ImageSource - // MARK: - Navigation Title - - private let title: String - // MARK: - Description Variables private let index: Int? @@ -49,10 +43,6 @@ struct ItemImageDetailsView: View { private let ratingType: RatingType? private let ratingVotes: Int? - // MARK: - RemoteImageInfo vs ImageInfo - - private let isLocal: Bool - // MARK: - Image Actions private let onSave: (() -> Void)? @@ -63,15 +53,9 @@ struct ItemImageDetailsView: View { @State private var error: Error? - // MARK: - Collection Layout - - @State - private var layout: CollectionVGridLayout = .minWidth(150) - // MARK: - Initializer init( - title: String, viewModel: ItemImagesViewModel, imageSource: ImageSource, index: Int? = nil, @@ -82,11 +66,9 @@ struct ItemImageDetailsView: View { rating: Double? = nil, ratingType: RatingType? = nil, ratingVotes: Int? = nil, - isLocal: Bool, onSave: (() -> Void)? = nil, onDelete: (() -> Void)? = nil ) { - self.title = title self._viewModel = ObservedObject(wrappedValue: viewModel) self.imageSource = imageSource self.index = index @@ -97,7 +79,6 @@ struct ItemImageDetailsView: View { self.rating = rating self.ratingType = ratingType self.ratingVotes = ratingVotes - self.isLocal = isLocal self.onSave = onSave self.onDelete = onDelete } @@ -106,7 +87,6 @@ struct ItemImageDetailsView: View { var body: some View { contentView - .navigationBarTitle(title.localizedCapitalized) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { router.dismissCoordinator() @@ -115,6 +95,11 @@ struct ItemImageDetailsView: View { if viewModel.backgroundStates.contains(.refreshing) { ProgressView() } + + if let onSave { + Button(L10n.save, action: onSave) + .buttonStyle(.toolbarPill) + } } .onReceive(viewModel.events) { event in switch event { @@ -132,7 +117,7 @@ struct ItemImageDetailsView: View { // MARK: - Content View @ViewBuilder - var contentView: some View { + private var contentView: some View { List { HeaderSection( imageSource: imageSource, @@ -151,17 +136,11 @@ struct ItemImageDetailsView: View { ratingVotes: ratingVotes ) - if isLocal, let onDelete { + if isEditing, let onDelete { DeleteButton { onDelete() } } } - .topBarTrailing { - if !isLocal, let onSave { - Button(L10n.save, action: onSave) - .buttonStyle(.toolbarPill) - } - } } } diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift similarity index 72% rename from Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift rename to Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift index 084c6a795..d1f48ac68 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift @@ -38,66 +38,17 @@ struct ItemImagesView: View { @State private var selectedType: ImageType? + @State + private var isFilePickerPresented = false // MARK: - Error State @State private var error: Error? - // MARK: - Ordered ImageTypes - - private var orderedItems: [ImageType] { - ImageType.allCases.sorted { lhs, rhs in - if lhs == .primary { return true } - if rhs == .primary { return false } - return lhs.rawValue.localizedCaseInsensitiveCompare(rhs.rawValue) == .orderedAscending - } - } - // MARK: - Body var body: some View { - contentView - .navigationBarTitle(L10n.images) - .navigationBarTitleDisplayMode(.inline) - .onFirstAppear { - viewModel.send(.refresh) - } - .navigationBarCloseButton { - router.dismissCoordinator() - } - .fileImporter( - isPresented: .constant(selectedType != nil), - allowedContentTypes: [.image], - allowsMultipleSelection: false - ) { - switch $0 { - case let .success(urls): - if let file = urls.first, let type = selectedType { - viewModel.send(.uploadImage(file: file, type: type)) - selectedType = nil - } - case let .failure(fileError): - error = fileError - selectedType = nil - } - } - .onReceive(viewModel.events) { event in - switch event { - case .updated: - break - case .deleted: - break - case let .error(eventError): - self.error = eventError - } - } - .errorMessage($error) - } - - // MARK: - Content View - - private var contentView: some View { ZStack { switch viewModel.state { case .content, .deleting, .updating: @@ -111,6 +62,41 @@ struct ItemImagesView: View { } } } + .navigationBarTitle(L10n.images) + .navigationBarTitleDisplayMode(.inline) + .onFirstAppear { + viewModel.send(.refresh) + } + .navigationBarCloseButton { + router.dismissCoordinator() + } + .fileImporter( + isPresented: $isFilePickerPresented, + allowedContentTypes: [.png, .jpeg, .heic], + allowsMultipleSelection: false + ) { + switch $0 { + case let .success(urls): + if let file = urls.first, let type = selectedType { + viewModel.send(.uploadImage(file: file, type: type)) + selectedType = nil + } + case let .failure(fileError): + error = fileError + selectedType = nil + } + } + .onReceive(viewModel.events) { event in + switch event { + case .updated: + break + case .deleted: + break + case let .error(eventError): + self.error = eventError + } + } + .errorMessage($error) } // MARK: - Image View @@ -118,10 +104,12 @@ struct ItemImagesView: View { @ViewBuilder private var imageView: some View { ScrollView { - ForEach(orderedItems, id: \.self) { imageType in + ForEach(ImageType.allCases.sorted(using: \.rawValue), id: \.self) { imageType in Section { imageScrollView(for: imageType) - Divider().padding(.vertical, 16) + + Divider() + .padding(.vertical, 16) } header: { sectionHeader(for: imageType) } @@ -170,6 +158,7 @@ struct ItemImagesView: View { Button(L10n.uploadFile, systemImage: "document.badge.plus") { selectedType = imageType + isFilePickerPresented = true } Button(L10n.uploadPhoto, systemImage: "photo.badge.plus") { @@ -187,6 +176,7 @@ struct ItemImagesView: View { // MARK: - Image Button + @ViewBuilder private func imageButton( imageInfo: ImageInfo, onSelect: @escaping () -> Void @@ -194,6 +184,7 @@ struct ItemImagesView: View { Button(action: onSelect) { ZStack { Color.secondarySystemFill + ImageView( imageInfo.itemImageSource( itemID: viewModel.item.id!, diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift index c10da1e9a..e7c4e05d6 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift @@ -13,11 +13,6 @@ import SwiftUI struct ItemPhotoCropView: View { - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - // MARK: - State, Observed, & Environment Objects @EnvironmentObject @@ -40,7 +35,6 @@ struct ItemPhotoCropView: View { var body: some View { PhotoCropView( - isReady: viewModel.state == .initial, isSaving: viewModel.state == .updating, image: image, cropShape: .rect, diff --git a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift index 7819305a1..bcbff1ae0 100644 --- a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift +++ b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift @@ -12,11 +12,6 @@ import SwiftUI struct PhotoCropView: View { - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - // MARK: - State, Observed, & Environment Objects @StateObject @@ -24,7 +19,6 @@ struct PhotoCropView: View { // MARK: - Image Variable - let isReady: Bool let isSaving: Bool let image: UIImage let cropShape: Mantis.CropShapeType @@ -39,38 +33,23 @@ struct PhotoCropView: View { initialImage: image, cropShape: cropShape, presetRatio: presetRatio, - proxy: proxy - ) { - onSave($0) - } + proxy: proxy, + onImageCropped: onSave + ) .topBarTrailing { - if isReady { - Button(L10n.rotate, systemImage: "rotate.right") { - proxy.rotate() - } - .foregroundStyle(.gray) + Button(L10n.rotate, systemImage: "rotate.right") { + proxy.rotate() } if isSaving { - Button(L10n.cancel) { - onCancel() - } - .foregroundStyle(.red) + Button(L10n.cancel, action: onCancel) + .buttonStyle(.toolbarPill(.red)) } else { - Button { + Button(L10n.save) { proxy.crop() - } label: { - Text(L10n.save) - .foregroundStyle(accentColor.overlayColor) - .font(.headline) - .padding(.vertical, 5) - .padding(.horizontal, 10) - .background { - accentColor - } - .clipShape(RoundedRectangle(cornerRadius: 10)) } + .buttonStyle(.toolbarPill) } } .toolbar { @@ -95,7 +74,7 @@ struct PhotoCropView: View { // MARK: - Photo Crop View -struct _PhotoCropView: UIViewControllerRepresentable { +private struct _PhotoCropView: UIViewControllerRepresentable { class Proxy: ObservableObject { diff --git a/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift index f9c856b0e..a95e01081 100644 --- a/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift +++ b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift @@ -75,12 +75,11 @@ struct PhotoPickerView: UIViewControllerRepresentable { let itemProvider = image.itemProvider - if itemProvider.canLoadObject(ofClass: UIImage.self) { - itemProvider.loadObject(ofClass: UIImage.self) { image, _ in - if let image = image as? UIImage { - self.onSelect?(image) - } - } + guard itemProvider.canLoadObject(ofClass: UIImage.self) else { return } + + itemProvider.loadObject(ofClass: UIImage.self) { image, _ in + guard let image = image as? UIImage else { return } + self.onSelect?(image) } } } diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift index e3357fcf8..6deca814b 100644 --- a/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift +++ b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift @@ -13,11 +13,6 @@ import SwiftUI struct UserProfileImageCropView: View { - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - // MARK: - State, Observed, & Environment Objects @EnvironmentObject @@ -39,7 +34,6 @@ struct UserProfileImageCropView: View { var body: some View { PhotoCropView( - isReady: viewModel.state == .initial, isSaving: viewModel.state == .uploading, image: image, cropShape: .square, From a1d75df3fb2f01c39c4648c9dd3c161ed6932890 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 7 Jan 2025 15:54:06 -0700 Subject: [PATCH 41/45] Update ItemImagesView.swift --- .../Views/ItemEditorView/ItemImages/ItemImagesView.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift index d1f48ac68..8732029e6 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift @@ -62,7 +62,7 @@ struct ItemImagesView: View { } } } - .navigationBarTitle(L10n.images) + .navigationTitle(L10n.images) .navigationBarTitleDisplayMode(.inline) .onFirstAppear { viewModel.send(.refresh) @@ -88,10 +88,7 @@ struct ItemImagesView: View { } .onReceive(viewModel.events) { event in switch event { - case .updated: - break - case .deleted: - break + case .updated, .deleted: () case let .error(eventError): self.error = eventError } From 8249bd96bb81488ea9e060f6efe8c5f328686774 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 17:07:18 -0700 Subject: [PATCH 42/45] Event Only on upload failures. --- .../ItemAdministration/ItemImagesViewModel.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift index 0a5594a71..129f7f8dc 100644 --- a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -126,14 +126,14 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { try await self.getAllImages() await MainActor.run { + self.state = .updating self.eventSubject.send(.updated) - _ = self.state = .updating } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { + self.state = .content self.eventSubject.send(.error(apiError)) - _ = self.state = .updating } } }.asAnyCancellable() @@ -160,7 +160,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .error(apiError) + self.state = .content self.eventSubject.send(.error(apiError)) } } @@ -188,7 +188,7 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .error(apiError) + self.state = .content self.eventSubject.send(.error(apiError)) } } @@ -210,13 +210,13 @@ class ItemImagesViewModel: ViewModel, Stateful, Eventful { try await refreshItem() await MainActor.run { - self.eventSubject.send(.deleted) self.state = .deleting + self.eventSubject.send(.deleted) } } catch { let apiError = JellyfinAPIError(error.localizedDescription) await MainActor.run { - self.state = .deleting + self.state = .content self.eventSubject.send(.error(apiError)) } } From 2fd52eb513a3dd77a9542a4a638d930b361531d1 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 17:08:49 -0700 Subject: [PATCH 43/45] Remove unnecessary ViewModel's from tvOS. --- Swiftfin.xcodeproj/project.pbxproj | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2448d555b..d6df6ded9 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -72,11 +72,9 @@ 4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; }; 4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */; }; - 4E37F6172D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */; }; 4E3A24DA2CFE34A00083A72C /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */; }; 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; - 4E45939F2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; 4E4593A32D04E2B500E277E1 /* ItemImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */; }; 4E4593A62D04E4E300E277E1 /* AddItemImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */; }; 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; @@ -95,13 +93,9 @@ 4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */; }; 4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */; }; 4E4E9C672CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */; }; - 4E4E9C682CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */; }; 4E4E9C6A2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */; }; - 4E4E9C6B2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */; }; 4E5071D72CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */; }; - 4E5071D82CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */; }; 4E5071DA2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */; }; - 4E5071DB2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */; }; 4E5071E42CFCEFD3003FA2AD /* AddItemElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */; }; 4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */; }; 4E537A842D03D11200659A1A /* ServerUserDeviceAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */; }; @@ -114,7 +108,6 @@ 4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; }; 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; }; 4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; }; - 4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; }; 4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; }; 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A002CEFE39900025C99 /* EditMetadataView.swift */; }; 4E661A0F2CEFE46300025C99 /* SeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0D2CEFE46300025C99 /* SeriesSection.swift */; }; @@ -235,7 +228,6 @@ 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.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 /* RemoteSearchResultRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */; }; @@ -5411,7 +5403,6 @@ C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */, E13D98EE2D0664C1005FE96D /* NotificationSet.swift in Sources */, E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */, - 4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */, E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */, E1E0BEB829EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */, E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */, @@ -5473,7 +5464,6 @@ E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */, E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */, E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */, - 4EE766F82D132054009658F0 /* IdentifyItemViewModel.swift in Sources */, E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */, E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */, BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */, @@ -5559,7 +5549,6 @@ E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */, E17DC74B2BE740D900B42379 /* StoredValues+Server.swift in Sources */, 4E0253BD2CBF0C06007EB9CD /* DeviceType.swift in Sources */, - 4E5071D82CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */, E10E842A29A587110064EA49 /* LoadingView.swift in Sources */, E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */, @@ -5587,7 +5576,6 @@ E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */, E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, E18A17F0298C68B700C22F62 /* Overlay.swift in Sources */, - 4E5071DB2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */, 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */, 4E7315752D1485C900EA2A95 /* UserProfileImage.swift in Sources */, E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */, @@ -5624,7 +5612,6 @@ E11042762B8013DF00821020 /* Stateful.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, E1575E66293E77B5001665B1 /* Poster.swift in Sources */, - 4E4E9C682CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */, E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */, E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */, 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */, @@ -5657,14 +5644,12 @@ E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */, E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */, E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */, - 4E37F6172D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */, 4E35CE672CBED8B600DBD886 /* ServerTicks.swift in Sources */, E193D549271941CC00900D82 /* UserSignInView.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, E148128628C15475003B8787 /* SortOrder+ItemSortOrder.swift in Sources */, E1CB75722C80E71800217C76 /* DirectPlayProfile.swift in Sources */, E1E1E24E28DF8A2E000DF5FD /* PreferenceKeys.swift in Sources */, - 4E4E9C6B2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */, E1575E9B293E7B1E001665B1 /* EnvironmentValue+Keys.swift in Sources */, E133328929538D8D00EE76AB /* Files.swift in Sources */, E154967A296CB4B000C4EF88 /* VideoPlayerSettingsView.swift in Sources */, @@ -5703,7 +5688,6 @@ 4E8F74B12CE03EB000CC8969 /* RefreshMetadataViewModel.swift in Sources */, E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */, E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */, - 4E45939F2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */, E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */, From 96eedc41b966801160357a393e81446eaae5e16b Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 17:13:02 -0700 Subject: [PATCH 44/45] Add dismiss action to RemoteSearchResultView. While I am doing this here, fix it there. --- .../IdentifyItemView/IdentifyItemView.swift | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift index a9b42100e..ef42169dc 100644 --- a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -74,13 +74,19 @@ struct IdentifyItemView: View { .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 - } + .sheet(item: $selectedResult) { + selectedResult = nil + } content: { result in + RemoteSearchResultView( + result: result, + onSave: { + selectedResult = nil + viewModel.send(.update(result)) + }, + onClose: { + selectedResult = nil + } + ) } .onReceive(viewModel.events) { events in switch events { From 8ce80aab4513aaf4fe80baeb53f19d5cc5c6c741 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 7 Jan 2025 17:27:35 -0700 Subject: [PATCH 45/45] Move From Coordinator -> .Sheet. This fixes the popping issue / delay requirement! --- .../Coordinators/ItemImagesCoordinator.swift | 49 ------------------- .../ItemImages/AddItemImageView.swift | 35 ++++++++++--- .../ItemImageDetailsView.swift | 47 +++++++----------- .../ItemImages/ItemImagesView.swift | 27 +++++++++- 4 files changed, 73 insertions(+), 85 deletions(-) diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift index 186e5a280..5adad0b97 100644 --- a/Shared/Coordinators/ItemImagesCoordinator.swift +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -24,17 +24,10 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { @ObservedObject private var viewModel: ItemImagesViewModel - // MARK: - Route to Delete Local Image - - @Route(.modal) - var deleteImage = makeDeleteImage - // MARK: - Route to Add Remote Image @Route(.push) var addImage = makeAddImage - @Route(.modal) - var selectImage = makeSelectImage // MARK: - Route to Photo Picker @@ -54,48 +47,6 @@ final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { AddItemImageView(viewModel: viewModel, imageType: imageType) } - func makeSelectImage(remoteImageInfo: RemoteImageInfo) -> NavigationViewCoordinator { - NavigationViewCoordinator { - ItemImageDetailsView( - viewModel: self.viewModel, - imageSource: ImageSource(url: URL(string: remoteImageInfo.url)), - width: remoteImageInfo.width, - height: remoteImageInfo.height, - language: remoteImageInfo.language, - provider: remoteImageInfo.providerName, - rating: remoteImageInfo.communityRating, - ratingType: remoteImageInfo.ratingType, - ratingVotes: remoteImageInfo.voteCount, - onSave: { - self.viewModel.send(.setImage(remoteImageInfo)) - } - ) - .navigationTitle(remoteImageInfo.type?.displayTitle ?? "") - } - } - - // MARK: - Delete Local Image View - - func makeDeleteImage(imageInfo: ImageInfo) -> NavigationViewCoordinator { - NavigationViewCoordinator { - ItemImageDetailsView( - viewModel: self.viewModel, - imageSource: imageInfo.itemImageSource( - itemID: self.viewModel.item.id!, - client: self.viewModel.userSession.client - ), - index: imageInfo.imageIndex, - width: imageInfo.width, - height: imageInfo.height, - onDelete: { - self.viewModel.send(.deleteImage(imageInfo)) - } - ) - .navigationTitle(imageInfo.imageType?.displayTitle ?? "") - .environment(\.isEditing, true) - } - } - // MARK: - Photo Picker View func makePhotoPicker(type: ImageType) -> NavigationViewCoordinator { diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift index f102c1b64..eca61aaa6 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift @@ -25,8 +25,10 @@ struct AddItemImageView: View { @StateObject private var remoteImageInfoViewModel: RemoteImageInfoViewModel - // MARK: - Dialog States + // MARK: - Dialog State + @State + private var selectedImage: RemoteImageInfo? @State private var error: Error? @@ -69,6 +71,30 @@ struct AddItemImageView: View { ProgressView() } } + .sheet(item: $selectedImage) { + selectedImage = nil + } content: { remoteImageInfo in + ItemImageDetailsView( + viewModel: viewModel, + imageSource: ImageSource(url: URL(string: remoteImageInfo.url)), + width: remoteImageInfo.width, + height: remoteImageInfo.height, + language: remoteImageInfo.language, + provider: remoteImageInfo.providerName, + rating: remoteImageInfo.communityRating, + ratingType: remoteImageInfo.ratingType, + ratingVotes: remoteImageInfo.voteCount, + onClose: { + selectedImage = nil + }, + onSave: { + viewModel.send(.setImage(remoteImageInfo)) + selectedImage = nil + } + ) + .navigationTitle(remoteImageInfo.type?.displayTitle ?? "") + .environment(\.isEditing, true) + } .onFirstAppear { remoteImageInfoViewModel.send(.refresh) } @@ -78,10 +104,7 @@ struct AddItemImageView: View { break case .updated: UIDevice.feedback(.success) - // TODO: Why does this crash without the delay? - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - router.pop() - } + router.pop() case let .error(eventError): UIDevice.feedback(.error) error = eventError @@ -115,7 +138,7 @@ struct AddItemImageView: View { private func imageButton(_ image: RemoteImageInfo?) -> some View { Button { if let image { - router.route(to: \.selectImage, image) + selectedImage = image } } label: { posterImage( diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift index b29671c41..d9d65d001 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -45,14 +45,10 @@ struct ItemImageDetailsView: View { // MARK: - Image Actions + private let onClose: () -> Void private let onSave: (() -> Void)? private let onDelete: (() -> Void)? - // MARK: - Error State - - @State - private var error: Error? - // MARK: - Initializer init( @@ -66,6 +62,7 @@ struct ItemImageDetailsView: View { rating: Double? = nil, ratingType: RatingType? = nil, ratingVotes: Int? = nil, + onClose: @escaping () -> Void, onSave: (() -> Void)? = nil, onDelete: (() -> Void)? = nil ) { @@ -79,6 +76,7 @@ struct ItemImageDetailsView: View { self.rating = rating self.ratingType = ratingType self.ratingVotes = ratingVotes + self.onClose = onClose self.onSave = onSave self.onDelete = onDelete } @@ -86,32 +84,23 @@ struct ItemImageDetailsView: View { // MARK: - Body var body: some View { - contentView - .navigationBarTitleDisplayMode(.inline) - .navigationBarCloseButton { - router.dismissCoordinator() - } - .topBarTrailing { - if viewModel.backgroundStates.contains(.refreshing) { - ProgressView() - } - - if let onSave { - Button(L10n.save, action: onSave) - .buttonStyle(.toolbarPill) + NavigationView { + contentView + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + onClose() } - } - .onReceive(viewModel.events) { event in - switch event { - case .deleted, .updated: - UIDevice.feedback(.success) - router.dismissCoordinator() - case let .error(eventError): - UIDevice.feedback(.error) - error = eventError + .topBarTrailing { + if viewModel.backgroundStates.contains(.refreshing) { + ProgressView() + } + + if let onSave { + Button(L10n.save, action: onSave) + .buttonStyle(.toolbarPill) + } } - } - .errorMessage($error) + } } // MARK: - Content View diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift index 8732029e6..0818f441a 100644 --- a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift @@ -36,6 +36,8 @@ struct ItemImagesView: View { // MARK: - Dialog State + @State + private var selectedImage: ImageInfo? @State private var selectedType: ImageType? @State @@ -70,6 +72,29 @@ struct ItemImagesView: View { .navigationBarCloseButton { router.dismissCoordinator() } + .sheet(item: $selectedImage) { + selectedImage = nil + } content: { imageInfo in + ItemImageDetailsView( + viewModel: viewModel, + imageSource: imageInfo.itemImageSource( + itemID: viewModel.item.id!, + client: viewModel.userSession.client + ), + index: imageInfo.imageIndex, + width: imageInfo.width, + height: imageInfo.height, + onClose: { + selectedImage = nil + }, + onDelete: { + viewModel.send(.deleteImage(imageInfo)) + selectedImage = nil + } + ) + .navigationTitle(imageInfo.imageType?.displayTitle ?? "") + .environment(\.isEditing, true) + } .fileImporter( isPresented: $isFilePickerPresented, allowedContentTypes: [.png, .jpeg, .heic], @@ -125,7 +150,7 @@ struct ItemImagesView: View { HStack { ForEach(imageArray, id: \.self) { imageInfo in imageButton(imageInfo: imageInfo) { - router.route(to: \.deleteImage, imageInfo) + selectedImage = imageInfo } } }