From c8acd780be75bd80f2769dfccc979c950107d7ad Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sun, 8 Dec 2024 23:57:16 -0700 Subject: [PATCH] Static Notification Payloads, Move more to `IdentifiedArray` (#1349) * wip * wip * wip * wip * clean up * clean up * Update VideoPlayerManager.swift * clean up --- RedrawOnNotificationView.swift | 15 +- .../OnReceiveNotificationModifier.swift | 10 +- .../ViewExtensions/ViewExtensions.swift | 13 +- Shared/Objects/NotificationSet.swift | 27 +++ Shared/Services/Notifications.swift | 184 ++++++++++++++++++ Shared/Services/SwiftfinNotifications.swift | 94 --------- .../ViewModels/ConnectToServerViewModel.swift | 2 +- Shared/ViewModels/HomeViewModel.swift | 7 +- .../DeleteItemViewModel.swift | 4 +- .../ItemEditorViewModel.swift | 2 +- .../RefreshMetadataViewModel.swift | 2 +- .../ItemViewModel/ItemViewModel.swift | 41 ++-- .../ItemViewModel/SeasonItemViewModel.swift | 17 +- .../ItemViewModel/SeriesItemViewModel.swift | 4 +- .../PagingLibraryViewModel.swift | 15 +- .../ServerConnectionViewModel.swift | 4 +- .../VideoPlayerManager.swift | 4 +- Swiftfin tvOS/App/SwiftfinApp.swift | 4 +- Swiftfin tvOS/Views/ConnectToServerView.swift | 2 +- .../Components/EpisodeHStack.swift | 4 +- .../EpisodeSelector/EpisodeSelector.swift | 34 ++-- .../Views/SelectUserView/SelectUserView.swift | 36 ++-- Swiftfin.xcodeproj/project.pbxproj | 26 ++- Swiftfin/App/SwiftfinApp.swift | 4 +- Swiftfin/Extensions/View/View-iOS.swift | 12 +- Swiftfin/Objects/AppURLHandler.swift | 3 +- .../AddServerUserView/AddServerUserView.swift | 2 +- .../ServerUsersView/ServerUsersView.swift | 3 +- Swiftfin/Views/ConnectToServerView.swift | 2 +- .../EpisodeSelector/EpisodeSelector.swift | 30 +-- .../Views/SelectUserView/SelectUserView.swift | 36 ++-- .../UserProfileSettingsView.swift | 2 +- 32 files changed, 376 insertions(+), 269 deletions(-) create mode 100644 Shared/Objects/NotificationSet.swift create mode 100644 Shared/Services/Notifications.swift delete mode 100644 Shared/Services/SwiftfinNotifications.swift diff --git a/RedrawOnNotificationView.swift b/RedrawOnNotificationView.swift index 0bcd8814..9b86d4a6 100644 --- a/RedrawOnNotificationView.swift +++ b/RedrawOnNotificationView.swift @@ -8,28 +8,23 @@ import SwiftUI -struct RedrawOnNotificationView: View { +struct RedrawOnNotificationView: View { @State private var id = 0 - private let name: NSNotification.Name + private let key: Notifications.Key

private let content: () -> Content - init(name: NSNotification.Name, @ViewBuilder content: @escaping () -> Content) { - self.name = name - self.content = content - } - - init(_ swiftfinNotification: Notifications.Key, @ViewBuilder content: @escaping () -> Content) { - self.name = swiftfinNotification.underlyingNotification.name + init(_ key: Notifications.Key

, @ViewBuilder content: @escaping () -> Content) { + self.key = key self.content = content } var body: some View { content() .id(id) - .onNotification(name) { _ in + .onNotification(key) { _ in id += 1 } } diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift index ec2606bd..1fd538e7 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift @@ -8,15 +8,13 @@ import SwiftUI -struct OnReceiveNotificationModifier: ViewModifier { +struct OnReceiveNotificationModifier>: ViewModifier { - let notification: NSNotification.Name - let onReceive: (Notification) -> Void + let key: K + let onReceive: (P) -> Void func body(content: Content) -> some View { content - .onReceive(NotificationCenter.default.publisher(for: notification)) { - onReceive($0) - } + .onReceive(key.publisher, perform: onReceive) } } diff --git a/Shared/Extensions/ViewExtensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions/ViewExtensions.swift index c56a1e9e..583cf7c5 100644 --- a/Shared/Extensions/ViewExtensions/ViewExtensions.swift +++ b/Shared/Extensions/ViewExtensions/ViewExtensions.swift @@ -314,19 +314,10 @@ extension View { } } - func onNotification(_ name: NSNotification.Name, perform action: @escaping (Notification) -> Void) -> some View { + func onNotification

(_ key: Notifications.Key

, perform action: @escaping (P) -> Void) -> some View { modifier( OnReceiveNotificationModifier( - notification: name, - onReceive: action - ) - ) - } - - func onNotification(_ swiftfinNotification: Notifications.Key, perform action: @escaping (Notification) -> Void) -> some View { - modifier( - OnReceiveNotificationModifier( - notification: swiftfinNotification.underlyingNotification.name, + key: key, onReceive: action ) ) diff --git a/Shared/Objects/NotificationSet.swift b/Shared/Objects/NotificationSet.swift new file mode 100644 index 00000000..5dc37da9 --- /dev/null +++ b/Shared/Objects/NotificationSet.swift @@ -0,0 +1,27 @@ +// +// 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 + +/// A container for `Notifications.Key`. +struct NotificationSet { + + private var names: Set = [] + + func contains

(_ key: Notifications.Key

) -> Bool { + names.contains(key.name.rawValue) + } + + mutating func insert

(_ key: Notifications.Key

) { + names.insert(key.name.rawValue) + } + + mutating func remove

(_ key: Notifications.Key

) { + names.remove(key.name.rawValue) + } +} diff --git a/Shared/Services/Notifications.swift b/Shared/Services/Notifications.swift new file mode 100644 index 00000000..ce726b23 --- /dev/null +++ b/Shared/Services/Notifications.swift @@ -0,0 +1,184 @@ +// +// 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 Factory +import Foundation +import JellyfinAPI +import UIKit + +extension Container { + var notificationCenter: Factory { + self { NotificationCenter.default }.singleton + } +} + +enum Notifications { + + typealias Keys = _AnyKey + + class _AnyKey { + typealias Key = Notifications.Key + } + + final class Key: _AnyKey { + + @Injected(\.notificationCenter) + private var notificationCenter + + let name: Notification.Name + + var rawValue: String { + name.rawValue + } + + init(_ string: String) { + self.name = Notification.Name(string) + } + + init(_ name: Notification.Name) { + self.name = name + } + + func post(_ payload: Payload) { + notificationCenter + .post( + name: name, + object: nil, + userInfo: ["payload": payload] + ) + } + + func post() where Payload == Void { + notificationCenter + .post( + name: name, + object: nil, + userInfo: nil + ) + } + + var publisher: AnyPublisher { + notificationCenter + .publisher(for: name) + .compactMap { notification in + notification.userInfo?["payload"] as? Payload + } + .eraseToAnyPublisher() + } + + func subscribe(_ object: Any, selector: Selector) { + notificationCenter.addObserver(object, selector: selector, name: name, object: nil) + } + } + + static subscript(key: Key) -> Key { + key + } +} + +// MARK: - Keys + +extension Notifications.Key { + + // MARK: - Authentication + + static var didSignIn: Key { + Key("didSignIn") + } + + static var didSignOut: Key { + Key("didSignOut") + } + + // MARK: - App Flow + + static var processDeepLink: Key { + Key("processDeepLink") + } + + static var didPurge: Key { + Key("didPurge") + } + + static var didChangeCurrentServerURL: Key { + Key("didChangeCurrentServerURL") + } + + static var didSendStopReport: Key { + Key("didSendStopReport") + } + + static var didRequestGlobalRefresh: Key { + Key("didRequestGlobalRefresh") + } + + static var didFailMigration: Key { + Key("didFailMigration") + } + + // MARK: - Media Items + + /// - Payload: The new item with updated metadata. + static var itemMetadataDidChange: Key { + Key("itemMetadataDidChange") + } + + static var itemShouldRefresh: Key<(itemID: String, parentID: String?)> { + Key("itemShouldRefresh") + } + + /// - Payload: The ID of the deleted item. + static var didDeleteItem: Key { + Key("didDeleteItem") + } + + // MARK: - Server + + static var didConnectToServer: Key { + Key("didConnectToServer") + } + + static var didDeleteServer: Key { + Key("didDeleteServer") + } + + // MARK: - User + + static var didChangeUserProfileImage: Key { + Key("didChangeUserProfileImage") + } + + static var didAddServerUser: Key { + Key("didAddServerUser") + } + + // MARK: - Playback + + static var didStartPlayback: Key { + Key("didStartPlayback") + } + + // MARK: - UIApplication + + static var applicationDidEnterBackground: Key { + Key(UIApplication.didEnterBackgroundNotification) + } + + static var applicationWillEnterForeground: Key { + Key(UIApplication.willEnterForegroundNotification) + } + + static var applicationWillResignActive: Key { + Key(UIApplication.willResignActiveNotification) + } + + static var applicationWillTerminate: Key { + Key(UIApplication.willTerminateNotification) + } +} diff --git a/Shared/Services/SwiftfinNotifications.swift b/Shared/Services/SwiftfinNotifications.swift deleted file mode 100644 index 8d30b367..00000000 --- a/Shared/Services/SwiftfinNotifications.swift +++ /dev/null @@ -1,94 +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 Factory -import Foundation - -class SwiftfinNotification { - - @Injected(\.notificationCenter) - private var notificationService - - let name: Notification.Name - - fileprivate init(_ notificationName: Notification.Name) { - self.name = notificationName - } - - func post(object: Any? = nil) { - notificationService.post(name: name, object: object) - } - - func subscribe(_ observer: Any, selector: Selector) { - notificationService.addObserver(observer, selector: selector, name: name, object: nil) - } - - func unsubscribe(_ observer: Any) { - notificationService.removeObserver(self, name: name, object: nil) - } - - var publisher: NotificationCenter.Publisher { - notificationService.publisher(for: name) - } -} - -extension Container { - var notificationCenter: Factory { self { NotificationCenter.default }.singleton } -} - -enum Notifications { - - struct Key: Hashable { - - static func == (lhs: Notifications.Key, rhs: Notifications.Key) -> Bool { - lhs.key == rhs.key - } - - func hash(into hasher: inout Hasher) { - hasher.combine(key) - } - - typealias NotificationKey = Notifications.Key - - let key: String - let underlyingNotification: SwiftfinNotification - - init(_ key: String) { - self.key = key - self.underlyingNotification = SwiftfinNotification(Notification.Name(key)) - } - } - - static subscript(key: Key) -> SwiftfinNotification { - key.underlyingNotification - } -} - -extension Notifications.Key { - - static let didSignIn = NotificationKey("didSignIn") - static let didSignOut = NotificationKey("didSignOut") - static let processDeepLink = NotificationKey("processDeepLink") - static let didPurge = NotificationKey("didPurge") - static let didChangeCurrentServerURL = NotificationKey("didChangeCurrentServerURL") - static let didSendStopReport = NotificationKey("didSendStopReport") - static let didRequestGlobalRefresh = NotificationKey("didRequestGlobalRefresh") - static let didFailMigration = NotificationKey("didFailMigration") - - static let itemMetadataDidChange = NotificationKey("itemMetadataDidChange") - static let didDeleteItem = NotificationKey("didDeleteItem") - - static let didConnectToServer = NotificationKey("didConnectToServer") - static let didDeleteServer = NotificationKey("didDeleteServer") - - static let didChangeUserProfileImage = NotificationKey("didChangeUserProfileImage") - - static let didStartPlayback = NotificationKey("didStartPlayback") - - static let didAddServerUser = NotificationKey("didStartPlayback") -} diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 98f86de6..480e095a 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -233,7 +233,7 @@ final class ConnectToServerViewModel: ViewModel, Eventful, Stateful { return editServer.state } - Notifications[.didChangeCurrentServerURL].post(object: newState) + Notifications[.didChangeCurrentServerURL].post(newState) } catch { logger.critical("\(error.localizedDescription)") } diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 5e843a5b..8454357e 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -54,7 +54,7 @@ final class HomeViewModel: ViewModel, Stateful { // TODO: replace with views checking what notifications were // posted since last disappear @Published - var notificationsReceived: Set = [] + var notificationsReceived: NotificationSet = .init() private var backgroundRefreshTask: AnyCancellable? private var refreshTask: AnyCancellable? @@ -65,13 +65,14 @@ final class HomeViewModel: ViewModel, Stateful { override init() { super.init() - Notifications[.itemMetadataDidChange].publisher + Notifications[.itemMetadataDidChange] + .publisher .sink { _ in // Necessary because when this notification is posted, even with asyncAfter, // the view will cause layout issues since it will redraw while in landscape. // TODO: look for better solution DispatchQueue.main.async { - self.notificationsReceived.insert(Notifications.Key.itemMetadataDidChange) + self.notificationsReceived.insert(.itemMetadataDidChange) } } .store(in: &cancellables) diff --git a/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift b/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift index 0e63da81..9a3f6732 100644 --- a/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift @@ -96,7 +96,7 @@ class DeleteItemViewModel: ViewModel, Stateful, Eventful { // MARK: Metadata Refresh Logic private func deleteItem() async throws { - guard let itemID = item?.id else { + guard let item, let itemID = item.id else { throw JellyfinAPIError(L10n.unknownError) } @@ -104,7 +104,7 @@ class DeleteItemViewModel: ViewModel, Stateful, Eventful { _ = try await userSession.client.send(request) await MainActor.run { - Notifications[.didDeleteItem].post(object: item) + Notifications[.didDeleteItem].post(itemID) self.item = nil } } diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift index 59d6e906..baa76dfa 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift @@ -237,7 +237,7 @@ class ItemEditorViewModel: ViewModel, Stateful, Eventful { try await refreshItem() await MainActor.run { - Notifications[.itemMetadataDidChange].post(object: newItem) + Notifications[.itemMetadataDidChange].post(newItem) } } diff --git a/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift b/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift index 5d3e9014..7d45c45f 100644 --- a/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift @@ -169,7 +169,7 @@ class RefreshMetadataViewModel: ViewModel, Stateful, Eventful { self.item = response.value self.progress = 0.0 - Notifications[.itemMetadataDidChange].post(object: item) + Notifications[.itemMetadataDidChange].post(item) } } } diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index 7dfa36f8..cebab770 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -89,23 +89,24 @@ class ItemViewModel: ViewModel, Stateful { self.item = item super.init() - // TODO: should replace with a more robust "PlaybackManager" - Notifications[.itemMetadataDidChange].publisher - .sink { [weak self] notification in - if let userInfo = notification.object as? [String: String] { - if let itemID = userInfo["itemID"], itemID == item.id { - Task { [weak self] in - await self?.send(.backgroundRefresh) - } - } else if let seriesID = userInfo["seriesID"], seriesID == item.id { - Task { [weak self] in - await self?.send(.backgroundRefresh) - } - } - } else if let newItem = notification.object as? BaseItemDto, newItem.id == self?.item.id { - Task { [weak self] in - await self?.send(.replace(newItem)) - } + Notifications[.itemShouldRefresh] + .publisher + .sink { itemID, parentID in + guard itemID == self.item.id || parentID == self.item.id else { return } + + Task { + await self.send(.backgroundRefresh) + } + } + .store(in: &cancellables) + + Notifications[.itemMetadataDidChange] + .publisher + .sink { newItem in + guard let newItemID = newItem.id, newItemID == self.item.id else { return } + + Task { + await self.send(.replace(newItem)) } } .store(in: &cancellables) @@ -314,6 +315,8 @@ class ItemViewModel: ViewModel, Stateful { private func setIsPlayed(_ isPlayed: Bool) async throws { + guard let itemID = item.id else { return } + let request: Request if isPlayed { @@ -329,9 +332,7 @@ class ItemViewModel: ViewModel, Stateful { } let _ = try await userSession.client.send(request) - - let ids = ["itemID": item.id] - Notifications[.itemMetadataDidChange].post(object: ids) + Notifications[.itemShouldRefresh].post((itemID, nil)) } private func setIsFavorite(_ isFavorite: Bool) async throws { diff --git a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift index 8fdac71c..0f9cb0be 100644 --- a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift @@ -13,10 +13,14 @@ import JellyfinAPI // Since we don't view care to view seasons directly, this doesn't subclass from `ItemViewModel`. // If we ever care for viewing seasons directly, subclass from that and have the library view model // as a property. -final class SeasonItemViewModel: PagingLibraryViewModel { +final class SeasonItemViewModel: PagingLibraryViewModel, Identifiable { let season: BaseItemDto + var id: String? { + season.id + } + init(season: BaseItemDto) { self.season = season super.init(parent: season) @@ -43,14 +47,3 @@ final class SeasonItemViewModel: PagingLibraryViewModel { return response.value.items ?? [] } } - -extension SeasonItemViewModel: Hashable { - - static func == (lhs: SeasonItemViewModel, rhs: SeasonItemViewModel) -> Bool { - lhs.parent as! BaseItemDto == rhs.parent as! BaseItemDto - } - - func hash(into hasher: inout Hasher) { - hasher.combine((parent as! BaseItemDto).hashValue) - } -} diff --git a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift index ae428806..bfed1f2c 100644 --- a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift @@ -10,8 +10,8 @@ import Combine import Defaults import Factory import Foundation +import IdentifiedCollections import JellyfinAPI -import OrderedCollections // TODO: care for one long episodes list? // - after SeasonItemViewModel is bidirectional @@ -19,7 +19,7 @@ import OrderedCollections final class SeriesItemViewModel: ItemViewModel { @Published - var seasons: OrderedSet = [] + var seasons: IdentifiedArrayOf = [] override func onRefresh() async throws { diff --git a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift index 7babfdcf..abe775bc 100644 --- a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift @@ -67,8 +67,9 @@ class PagingLibraryViewModel: ViewModel, Eventfu @Published final var backgroundStates: OrderedSet = [] + /// - Keys: the `hashValue` of the `Element.ID` @Published - final var elements: IdentifiedArrayOf + final var elements: IdentifiedArray @Published final var state: State = .initial @Published @@ -104,7 +105,7 @@ class PagingLibraryViewModel: ViewModel, Eventfu parent: (any LibraryParent)? = nil ) { self.filterViewModel = nil - self.elements = IdentifiedArray(uniqueElements: data) + self.elements = IdentifiedArray(uniqueElements: data, id: \.id.hashValue) self.isStatic = true self.hasNextPage = false self.pageSize = DefaultPageSize @@ -131,7 +132,7 @@ class PagingLibraryViewModel: ViewModel, Eventfu filters: ItemFilterCollection? = nil, pageSize: Int = DefaultPageSize ) { - self.elements = IdentifiedArray() + self.elements = IdentifiedArray(id: \.id.hashValue) self.isStatic = false self.pageSize = pageSize self.parent = parent @@ -160,10 +161,10 @@ class PagingLibraryViewModel: ViewModel, Eventfu super.init() - Notifications[.didDeleteItem].publisher - .sink(receiveCompletion: { _ in }) { [weak self] notification in - guard let item = notification.object as? Element else { return } - self?.elements.remove(item) + Notifications[.didDeleteItem] + .publisher + .sink { id in + self.elements.remove(id: id.hashValue) } .store(in: &cancellables) diff --git a/Shared/ViewModels/ServerConnectionViewModel.swift b/Shared/ViewModels/ServerConnectionViewModel.swift index 1ec3d7c4..778e7e2f 100644 --- a/Shared/ViewModels/ServerConnectionViewModel.swift +++ b/Shared/ViewModels/ServerConnectionViewModel.swift @@ -50,7 +50,7 @@ class ServerConnectionViewModel: ViewModel { UserDefaults.userSuite(id: user.id).removeAll() } - Notifications[.didDeleteServer].post(object: server) + Notifications[.didDeleteServer].post(server) } catch { logger.critical("Unable to delete server: \(server.name)") } @@ -67,7 +67,7 @@ class ServerConnectionViewModel: ViewModel { return storedServer.state } - Notifications[.didChangeCurrentServerURL].post(object: newState) + Notifications[.didChangeCurrentServerURL].post(newState) self.server = newState } catch { diff --git a/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift b/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift index f405a513..006bc0e9 100644 --- a/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift +++ b/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift @@ -213,8 +213,10 @@ class VideoPlayerManager: ViewModel { func sendStopReport() { + // TODO: This entire system is being redone in other PRs, + // can ignore the fact this is commented out for now. let ids = ["itemID": currentViewModel.item.id, "seriesID": currentViewModel.item.parentID] - Notifications[.itemMetadataDidChange].post(object: ids) +// Notifications[.itemMetadataDidChange].post(ids) #if DEBUG guard Defaults[.sendProgressReports] else { return } diff --git a/Swiftfin tvOS/App/SwiftfinApp.swift b/Swiftfin tvOS/App/SwiftfinApp.swift index 8462a325..26eb28c2 100644 --- a/Swiftfin tvOS/App/SwiftfinApp.swift +++ b/Swiftfin tvOS/App/SwiftfinApp.swift @@ -58,10 +58,10 @@ struct SwiftfinApp: App { WindowGroup { MainCoordinator() .view() - .onNotification(UIApplication.didEnterBackgroundNotification) { _ in + .onNotification(.applicationDidEnterBackground) { Defaults[.backgroundTimeStamp] = Date.now } - .onNotification(UIApplication.willEnterForegroundNotification) { _ in + .onNotification(.applicationWillEnterForeground) { // TODO: needs to check if any background playback is happening let backgroundedInterval = Date.now.timeIntervalSince(Defaults[.backgroundTimeStamp]) diff --git a/Swiftfin tvOS/Views/ConnectToServerView.swift b/Swiftfin tvOS/Views/ConnectToServerView.swift index 70744865..bb9cb8f6 100644 --- a/Swiftfin tvOS/Views/ConnectToServerView.swift +++ b/Swiftfin tvOS/Views/ConnectToServerView.swift @@ -153,7 +153,7 @@ struct ConnectToServerView: View { .onReceive(viewModel.events) { event in switch event { case let .connected(server): - Notifications[.didConnectToServer].post(object: server) + Notifications[.didConnectToServer].post(server) router.popLast() case let .duplicateServer(server): duplicateServer = server diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index b672c604..2cf9f967 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -79,8 +79,8 @@ extension SeriesEpisodeSelector { onContentFocus: { focusedEpisodeID = lastFocusedEpisodeID }, top: "seasons" ) - .onChange(of: viewModel) { _, newValue in - lastFocusedEpisodeID = newValue.elements.first?.id + .onChange(of: viewModel.id) { + lastFocusedEpisodeID = viewModel.elements.first?.id } .onChange(of: focusedEpisodeID) { _, newValue in guard let newValue else { return } diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift index a103fa8a..f0071d02 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift @@ -21,15 +21,19 @@ struct SeriesEpisodeSelector: View { @State private var didSelectPlayButtonSeason = false @State - private var selection: SeasonItemViewModel? + private var selection: SeasonItemViewModel.ID? + + private var selectionViewModel: SeasonItemViewModel? { + viewModel.seasons.first(where: { $0.id == selection }) + } var body: some View { VStack(spacing: 0) { SeasonsHStack(viewModel: viewModel, selection: $selection) .environmentObject(parentFocusGuide) - if let selection { - EpisodeHStack(viewModel: selection, playButtonItem: viewModel.playButtonItem) + if let selectionViewModel { + EpisodeHStack(viewModel: selectionViewModel, playButtonItem: viewModel.playButtonItem) .environmentObject(parentFocusGuide) } else { LoadingHStack() @@ -40,17 +44,17 @@ struct SeriesEpisodeSelector: View { guard !didSelectPlayButtonSeason else { return } didSelectPlayButtonSeason = true - if let season = viewModel.seasons.first(where: { $0.season.id == newValue.seasonID }) { - selection = season + if let playButtonSeason = viewModel.seasons.first(where: { $0.id == newValue.seasonID }) { + selection = playButtonSeason.id } else { - selection = viewModel.seasons.first + selection = viewModel.seasons.first?.id } } - .onChange(of: selection) { _, newValue in - guard let newValue else { return } + .onChange(of: selection) { _ in + guard let selectionViewModel else { return } - if newValue.state == .initial { - newValue.send(.refresh) + if selectionViewModel.state == .initial { + selectionViewModel.send(.refresh) } } } @@ -66,31 +70,31 @@ extension SeriesEpisodeSelector { private var focusGuide: FocusGuide @FocusState - private var focusedSeason: SeasonItemViewModel? + private var focusedSeason: SeasonItemViewModel.ID? @ObservedObject var viewModel: SeriesItemViewModel - var selection: Binding + var selection: Binding var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack { - ForEach(viewModel.seasons, id: \.season.id) { seasonViewModel in + ForEach(viewModel.seasons) { seasonViewModel in Button { Text(seasonViewModel.season.displayTitle) .font(.headline) .fontWeight(.semibold) .padding(.vertical, 10) .padding(.horizontal, 20) - .if(selection.wrappedValue == seasonViewModel) { text in + .if(selection.wrappedValue == seasonViewModel.id) { text in text .background(Color.white) .foregroundColor(.black) } } .buttonStyle(.card) - .focused($focusedSeason, equals: seasonViewModel) + .focused($focusedSeason, equals: seasonViewModel.id) } } .focusGuide( diff --git a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift index 869008ed..e0899e7b 100644 --- a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift @@ -310,33 +310,27 @@ struct SelectUserView: View { Notifications[.didSignIn].post() } } - .onNotification(.didConnectToServer) { notification in - if let server = notification.object as? ServerState { - viewModel.send(.getServers) - serverSelection = .server(id: server.id) - } + .onNotification(.didConnectToServer) { server in + viewModel.send(.getServers) + serverSelection = .server(id: server.id) } - .onNotification(.didChangeCurrentServerURL) { notification in - if let server = notification.object as? ServerState { - viewModel.send(.getServers) - serverSelection = .server(id: server.id) - } + .onNotification(.didChangeCurrentServerURL) { server in + viewModel.send(.getServers) + serverSelection = .server(id: server.id) } - .onNotification(.didDeleteServer) { notification in + .onNotification(.didDeleteServer) { server in viewModel.send(.getServers) - if let server = notification.object as? ServerState { - if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id { - if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first { - serverSelection = .server(id: first.id) - } else { - serverSelection = .all - } + if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id { + if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first { + serverSelection = .server(id: first.id) + } else { + serverSelection = .all } - - // change splash screen selection if necessary -// selectUserAllServersSplashscreen = serverSelection } + + // change splash screen selection if necessary +// selectUserAllServersSplashscreen = serverSelection } } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 3549bd95..fad95049 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -545,6 +545,8 @@ E13AF3B828A0C598009093AB /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B728A0C598009093AB /* NukeExtensions */; }; E13AF3BA28A0C598009093AB /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B928A0C598009093AB /* NukeUI */; }; E13AF3BC28A0C59E009093AB /* BlurHashKit in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3BB28A0C59E009093AB /* BlurHashKit */; }; + E13D98ED2D0664C1005FE96D /* NotificationSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13D98EC2D0664C1005FE96D /* NotificationSet.swift */; }; + E13D98EE2D0664C1005FE96D /* NotificationSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13D98EC2D0664C1005FE96D /* NotificationSet.swift */; }; E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; }; E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3C52716499E009D4DAF /* CoreStore */; }; E13DD3C827164B1E009D4DAF /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDevice.swift */; }; @@ -623,8 +625,8 @@ E1549663296CA2EF00C4EF88 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549657296CA2EF00C4EF88 /* UserSession.swift */; }; E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549658296CA2EF00C4EF88 /* SwiftfinStore.swift */; }; E1549665296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549658296CA2EF00C4EF88 /* SwiftfinStore.swift */; }; - E1549666296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549659296CA2EF00C4EF88 /* SwiftfinNotifications.swift */; }; - E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549659296CA2EF00C4EF88 /* SwiftfinNotifications.swift */; }; + E1549666296CA2EF00C4EF88 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549659296CA2EF00C4EF88 /* Notifications.swift */; }; + E1549667296CA2EF00C4EF88 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549659296CA2EF00C4EF88 /* Notifications.swift */; }; E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965B296CA2EF00C4EF88 /* DownloadManager.swift */; }; E154966B296CA2EF00C4EF88 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965B296CA2EF00C4EF88 /* DownloadManager.swift */; }; E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965D296CA2EF00C4EF88 /* LogManager.swift */; }; @@ -1560,6 +1562,7 @@ E139CC1C28EC836F00688DE2 /* ChapterOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterOverlay.swift; sourceTree = ""; }; E139CC1E28EC83E400688DE2 /* Int.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int.swift; sourceTree = ""; }; E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = ""; }; + E13D98EC2D0664C1005FE96D /* NotificationSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSet.swift; sourceTree = ""; }; E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E13DD3C727164B1E009D4DAF /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInViewModel.swift; sourceTree = ""; }; @@ -1605,7 +1608,7 @@ E1549656296CA2EF00C4EF88 /* SwiftfinDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftfinDefaults.swift; sourceTree = ""; }; E1549657296CA2EF00C4EF88 /* UserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; E1549658296CA2EF00C4EF88 /* SwiftfinStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = ""; }; - E1549659296CA2EF00C4EF88 /* SwiftfinNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftfinNotifications.swift; sourceTree = ""; }; + E1549659296CA2EF00C4EF88 /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; E154965B296CA2EF00C4EF88 /* DownloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; E154965D296CA2EF00C4EF88 /* LogManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineEnumToggle.swift; sourceTree = ""; }; @@ -2777,6 +2780,7 @@ E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */, E1DE2B4E2B983F3200F6715F /* LibraryParent */, 4E2AC4C02C6C48EB00DD600D /* MediaComponents */, + E13D98EC2D0664C1005FE96D /* NotificationSet.swift */, E1AA331E2782639D00F6439C /* OverlayType.swift */, E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, 4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */, @@ -3747,8 +3751,8 @@ E1549655296CA2EF00C4EF88 /* DownloadTask.swift */, E19D41A92BF077130082B8B2 /* Keychain.swift */, E154965D296CA2EF00C4EF88 /* LogManager.swift */, + E1549659296CA2EF00C4EF88 /* Notifications.swift */, E1549656296CA2EF00C4EF88 /* SwiftfinDefaults.swift */, - E1549659296CA2EF00C4EF88 /* SwiftfinNotifications.swift */, E1549657296CA2EF00C4EF88 /* UserSession.swift */, ); path = Services; @@ -4951,6 +4955,7 @@ E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */, E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */, 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 */, @@ -5070,7 +5075,7 @@ 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */, 4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, - E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */, + E1549667296CA2EF00C4EF88 /* Notifications.swift in Sources */, E150C0BB2BFD44F500944FFA /* ImagePipeline.swift in Sources */, E11E0E8D2BF7E76F007676DD /* DataCache.swift in Sources */, E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */, @@ -5326,6 +5331,7 @@ E17FB55528C1250B00311DFE /* SimilarItemsHStack.swift in Sources */, C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */, 5364F455266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */, + E13D98ED2D0664C1005FE96D /* NotificationSet.swift in Sources */, E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, 4E5071D72CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */, E1B33ECF28EB6EA90073B0FD /* OverlayMenu.swift in Sources */, @@ -5622,7 +5628,7 @@ E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */, E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */, E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */, - E1549666296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */, + E1549666296CA2EF00C4EF88 /* Notifications.swift in Sources */, 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */, E1A1528528FD191A00600579 /* TextPair.swift in Sources */, 6334175D287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift in Sources */, @@ -6228,7 +6234,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -6244,7 +6250,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -6268,7 +6274,7 @@ CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -6284,7 +6290,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; diff --git a/Swiftfin/App/SwiftfinApp.swift b/Swiftfin/App/SwiftfinApp.swift index ca0ec2c8..66a0e5fd 100644 --- a/Swiftfin/App/SwiftfinApp.swift +++ b/Swiftfin/App/SwiftfinApp.swift @@ -97,10 +97,10 @@ struct SwiftfinApp: App { WindowGroup { versionedView .ignoresSafeArea() - .onNotification(UIApplication.didEnterBackgroundNotification) { _ in + .onNotification(.applicationDidEnterBackground) { Defaults[.backgroundTimeStamp] = Date.now } - .onNotification(UIApplication.willEnterForegroundNotification) { _ in + .onNotification(.applicationWillEnterForeground) { // TODO: needs to check if any background playback is happening // - atow, background video playback isn't officially supported diff --git a/Swiftfin/Extensions/View/View-iOS.swift b/Swiftfin/Extensions/View/View-iOS.swift index 2d23e152..d4d22ad4 100644 --- a/Swiftfin/Extensions/View/View-iOS.swift +++ b/Swiftfin/Extensions/View/View-iOS.swift @@ -55,15 +55,21 @@ extension View { } func onAppDidEnterBackground(_ action: @escaping () -> Void) -> some View { - onNotification(UIApplication.didEnterBackgroundNotification, perform: { _ in action() }) + onNotification(.applicationDidEnterBackground) { + action() + } } func onAppWillResignActive(_ action: @escaping () -> Void) -> some View { - onNotification(UIApplication.willResignActiveNotification, perform: { _ in action() }) + onNotification(.applicationWillResignActive) { _ in + action() + } } func onAppWillTerminate(_ action: @escaping () -> Void) -> some View { - onNotification(UIApplication.willTerminateNotification, perform: { _ in action() }) + onNotification(.applicationWillTerminate) { _ in + action() + } } @ViewBuilder diff --git a/Swiftfin/Objects/AppURLHandler.swift b/Swiftfin/Objects/AppURLHandler.swift index 044a6a83..e66f41c0 100644 --- a/Swiftfin/Objects/AppURLHandler.swift +++ b/Swiftfin/Objects/AppURLHandler.swift @@ -81,7 +81,8 @@ extension AppURLHandler { // It would be nice if the ItemViewModel could be initialized to id later. getItem(userID: userID, itemID: itemID) { item in guard let item = item else { return } - Notifications[.processDeepLink].post(object: DeepLink.item(item)) + // TODO: reimplement URL handling +// Notifications[.processDeepLink].post(DeepLink.item(item)) } return true diff --git a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift b/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift index 414f00dd..e423d194 100644 --- a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift +++ b/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift @@ -115,7 +115,7 @@ struct AddServerUserView: View { UIDevice.feedback(.success) router.dismissCoordinator { - Notifications[.didAddServerUser].post(object: newUser) + Notifications[.didAddServerUser].post(newUser) } } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift index 8cf69690..8a727547 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift @@ -147,8 +147,7 @@ struct ServerUsersView: View { } message: { Text(L10n.deleteUserSelfDeletion(viewModel.userSession.user.username)) } - .onNotification(.didAddServerUser) { notification in - let newUser = notification.object as! UserDto + .onNotification(.didAddServerUser) { newUser in viewModel.send(.appendUser(newUser)) router.route(to: \.userDetails, newUser) } diff --git a/Swiftfin/Views/ConnectToServerView.swift b/Swiftfin/Views/ConnectToServerView.swift index dda161da..3de3934d 100644 --- a/Swiftfin/Views/ConnectToServerView.swift +++ b/Swiftfin/Views/ConnectToServerView.swift @@ -42,7 +42,7 @@ struct ConnectToServerView: View { case let .connected(server): UIDevice.feedback(.success) - Notifications[.didConnectToServer].post(object: server) + Notifications[.didConnectToServer].post(server) router.popLast() case let .duplicateServer(server): UIDevice.feedback(.warning) diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift index a1d6eb90..54fa6b31 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift @@ -20,16 +20,20 @@ struct SeriesEpisodeSelector: View { @State private var didSelectPlayButtonSeason = false @State - private var selection: SeasonItemViewModel? + private var selection: SeasonItemViewModel.ID? + + private var selectionViewModel: SeasonItemViewModel? { + viewModel.seasons.first(where: { $0.id == selection }) + } @ViewBuilder private var seasonSelectorMenu: some View { Menu { ForEach(viewModel.seasons, id: \.season.id) { seasonViewModel in Button { - selection = seasonViewModel + selection = seasonViewModel.id } label: { - if seasonViewModel == selection { + if seasonViewModel.id == selection { Label(seasonViewModel.season.displayTitle, systemImage: "checkmark") } else { Text(seasonViewModel.season.displayTitle) @@ -38,7 +42,7 @@ struct SeriesEpisodeSelector: View { } } label: { Label( - selection?.season.displayTitle ?? .emptyDash, + selectionViewModel?.season.displayTitle ?? .emptyDash, systemImage: "chevron.down" ) .labelStyle(.episodeSelector) @@ -52,8 +56,8 @@ struct SeriesEpisodeSelector: View { .edgePadding([.bottom, .horizontal]) Group { - if let selection { - EpisodeHStack(viewModel: selection, playButtonItem: viewModel.playButtonItem) + if let selectionViewModel { + EpisodeHStack(viewModel: selectionViewModel, playButtonItem: viewModel.playButtonItem) } else { LoadingHStack() } @@ -65,17 +69,17 @@ struct SeriesEpisodeSelector: View { guard !didSelectPlayButtonSeason else { return } didSelectPlayButtonSeason = true - if let season = viewModel.seasons.first(where: { $0.season.id == newValue.seasonID }) { - selection = season + if let playButtonSeason = viewModel.seasons.first(where: { $0.id == newValue.seasonID }) { + selection = playButtonSeason.id } else { - selection = viewModel.seasons.first + selection = viewModel.seasons.first?.id } } - .onChange(of: selection) { newValue in - guard let newValue else { return } + .onChange(of: selection) { _ in + guard let selectionViewModel else { return } - if newValue.state == .initial { - newValue.send(.refresh) + if selectionViewModel.state == .initial { + selectionViewModel.send(.refresh) } } } diff --git a/Swiftfin/Views/SelectUserView/SelectUserView.swift b/Swiftfin/Views/SelectUserView/SelectUserView.swift index 87b89fbb..00ee2a42 100644 --- a/Swiftfin/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin/Views/SelectUserView/SelectUserView.swift @@ -566,33 +566,27 @@ struct SelectUserView: View { Notifications[.didSignIn].post() } } - .onNotification(.didConnectToServer) { notification in - if let server = notification.object as? ServerState { - viewModel.send(.getServers) - serverSelection = .server(id: server.id) - } + .onNotification(.didConnectToServer) { server in + viewModel.send(.getServers) + serverSelection = .server(id: server.id) } - .onNotification(.didChangeCurrentServerURL) { notification in - if let server = notification.object as? ServerState { - viewModel.send(.getServers) - serverSelection = .server(id: server.id) - } + .onNotification(.didChangeCurrentServerURL) { server in + viewModel.send(.getServers) + serverSelection = .server(id: server.id) } - .onNotification(.didDeleteServer) { notification in + .onNotification(.didDeleteServer) { server in viewModel.send(.getServers) - if let server = notification.object as? ServerState { - if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id { - if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first { - serverSelection = .server(id: first.id) - } else { - serverSelection = .all - } + if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id { + if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first { + serverSelection = .server(id: first.id) + } else { + serverSelection = .all } - - // change splash screen selection if necessary - selectUserAllServersSplashscreen = serverSelection } + + // change splash screen selection if necessary + selectUserAllServersSplashscreen = serverSelection } .alert( Text("Delete User"), diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift index 4e4ff71e..afebdcd6 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift @@ -28,7 +28,7 @@ struct UserProfileSettingsView: View { @ViewBuilder private var imageView: some View { - RedrawOnNotificationView(name: .init("didChangeUserProfileImage")) { + RedrawOnNotificationView(.didChangeUserProfileImage) { ImageView( viewModel.userSession.user.profileImageSource( client: viewModel.userSession.client,