Static Notification Payloads, Move more to `IdentifiedArray` (#1349)
* wip * wip * wip * wip * clean up * clean up * Update VideoPlayerManager.swift * clean up
This commit is contained in:
parent
e856303181
commit
c8acd780be
|
@ -8,28 +8,23 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct RedrawOnNotificationView<Content: View>: View {
|
||||
struct RedrawOnNotificationView<Content: View, P>: View {
|
||||
|
||||
@State
|
||||
private var id = 0
|
||||
|
||||
private let name: NSNotification.Name
|
||||
private let key: Notifications.Key<P>
|
||||
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<P>, @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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,15 +8,13 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct OnReceiveNotificationModifier: ViewModifier {
|
||||
struct OnReceiveNotificationModifier<P, K: Notifications.Key<P>>: 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -314,19 +314,10 @@ extension View {
|
|||
}
|
||||
}
|
||||
|
||||
func onNotification(_ name: NSNotification.Name, perform action: @escaping (Notification) -> Void) -> some View {
|
||||
func onNotification<P>(_ key: Notifications.Key<P>, 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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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<String> = []
|
||||
|
||||
func contains<P>(_ key: Notifications.Key<P>) -> Bool {
|
||||
names.contains(key.name.rawValue)
|
||||
}
|
||||
|
||||
mutating func insert<P>(_ key: Notifications.Key<P>) {
|
||||
names.insert(key.name.rawValue)
|
||||
}
|
||||
|
||||
mutating func remove<P>(_ key: Notifications.Key<P>) {
|
||||
names.remove(key.name.rawValue)
|
||||
}
|
||||
}
|
|
@ -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<NotificationCenter> {
|
||||
self { NotificationCenter.default }.singleton
|
||||
}
|
||||
}
|
||||
|
||||
enum Notifications {
|
||||
|
||||
typealias Keys = _AnyKey
|
||||
|
||||
class _AnyKey {
|
||||
typealias Key = Notifications.Key
|
||||
}
|
||||
|
||||
final class Key<Payload>: _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<Payload, Never> {
|
||||
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<Payload>(key: Key<Payload>) -> Key<Payload> {
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keys
|
||||
|
||||
extension Notifications.Key {
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
static var didSignIn: Key<Void> {
|
||||
Key("didSignIn")
|
||||
}
|
||||
|
||||
static var didSignOut: Key<Void> {
|
||||
Key("didSignOut")
|
||||
}
|
||||
|
||||
// MARK: - App Flow
|
||||
|
||||
static var processDeepLink: Key<Void> {
|
||||
Key("processDeepLink")
|
||||
}
|
||||
|
||||
static var didPurge: Key<Void> {
|
||||
Key("didPurge")
|
||||
}
|
||||
|
||||
static var didChangeCurrentServerURL: Key<ServerState> {
|
||||
Key("didChangeCurrentServerURL")
|
||||
}
|
||||
|
||||
static var didSendStopReport: Key<Void> {
|
||||
Key("didSendStopReport")
|
||||
}
|
||||
|
||||
static var didRequestGlobalRefresh: Key<Void> {
|
||||
Key("didRequestGlobalRefresh")
|
||||
}
|
||||
|
||||
static var didFailMigration: Key<Void> {
|
||||
Key("didFailMigration")
|
||||
}
|
||||
|
||||
// MARK: - Media Items
|
||||
|
||||
/// - Payload: The new item with updated metadata.
|
||||
static var itemMetadataDidChange: Key<BaseItemDto> {
|
||||
Key("itemMetadataDidChange")
|
||||
}
|
||||
|
||||
static var itemShouldRefresh: Key<(itemID: String, parentID: String?)> {
|
||||
Key("itemShouldRefresh")
|
||||
}
|
||||
|
||||
/// - Payload: The ID of the deleted item.
|
||||
static var didDeleteItem: Key<String> {
|
||||
Key("didDeleteItem")
|
||||
}
|
||||
|
||||
// MARK: - Server
|
||||
|
||||
static var didConnectToServer: Key<ServerState> {
|
||||
Key("didConnectToServer")
|
||||
}
|
||||
|
||||
static var didDeleteServer: Key<ServerState> {
|
||||
Key("didDeleteServer")
|
||||
}
|
||||
|
||||
// MARK: - User
|
||||
|
||||
static var didChangeUserProfileImage: Key<Void> {
|
||||
Key("didChangeUserProfileImage")
|
||||
}
|
||||
|
||||
static var didAddServerUser: Key<UserDto> {
|
||||
Key("didAddServerUser")
|
||||
}
|
||||
|
||||
// MARK: - Playback
|
||||
|
||||
static var didStartPlayback: Key<Void> {
|
||||
Key("didStartPlayback")
|
||||
}
|
||||
|
||||
// MARK: - UIApplication
|
||||
|
||||
static var applicationDidEnterBackground: Key<Void> {
|
||||
Key(UIApplication.didEnterBackgroundNotification)
|
||||
}
|
||||
|
||||
static var applicationWillEnterForeground: Key<Void> {
|
||||
Key(UIApplication.willEnterForegroundNotification)
|
||||
}
|
||||
|
||||
static var applicationWillResignActive: Key<Void> {
|
||||
Key(UIApplication.willResignActiveNotification)
|
||||
}
|
||||
|
||||
static var applicationWillTerminate: Key<Void> {
|
||||
Key(UIApplication.willTerminateNotification)
|
||||
}
|
||||
}
|
|
@ -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<NotificationCenter> { 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")
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
|
|
|
@ -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<Notifications.Key> = []
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,7 +237,7 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
|
|||
try await refreshItem()
|
||||
|
||||
await MainActor.run {
|
||||
Notifications[.itemMetadataDidChange].post(object: newItem)
|
||||
Notifications[.itemMetadataDidChange].post(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
Notifications[.itemShouldRefresh]
|
||||
.publisher
|
||||
.sink { itemID, parentID in
|
||||
guard itemID == self.item.id || parentID == self.item.id else { return }
|
||||
|
||||
Task {
|
||||
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))
|
||||
}
|
||||
.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<UserItemDataDto>
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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<BaseItemDto> {
|
||||
final class SeasonItemViewModel: PagingLibraryViewModel<BaseItemDto>, 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<BaseItemDto> {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SeasonItemViewModel> = []
|
||||
var seasons: IdentifiedArrayOf<SeasonItemViewModel> = []
|
||||
|
||||
override func onRefresh() async throws {
|
||||
|
||||
|
|
|
@ -67,8 +67,9 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventfu
|
|||
|
||||
@Published
|
||||
final var backgroundStates: OrderedSet<BackgroundState> = []
|
||||
/// - Keys: the `hashValue` of the `Element.ID`
|
||||
@Published
|
||||
final var elements: IdentifiedArrayOf<Element>
|
||||
final var elements: IdentifiedArray<Int, Element>
|
||||
@Published
|
||||
final var state: State = .initial
|
||||
@Published
|
||||
|
@ -104,7 +105,7 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: 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<Element: Poster & Identifiable>: 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<Element: Poster & Identifiable>: 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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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<SeasonItemViewModel?>
|
||||
var selection: Binding<SeasonItemViewModel.ID?>
|
||||
|
||||
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(
|
||||
|
|
|
@ -310,22 +310,17 @@ struct SelectUserView: View {
|
|||
Notifications[.didSignIn].post()
|
||||
}
|
||||
}
|
||||
.onNotification(.didConnectToServer) { notification in
|
||||
if let server = notification.object as? ServerState {
|
||||
.onNotification(.didConnectToServer) { server in
|
||||
viewModel.send(.getServers)
|
||||
serverSelection = .server(id: server.id)
|
||||
}
|
||||
}
|
||||
.onNotification(.didChangeCurrentServerURL) { notification in
|
||||
if let server = notification.object as? ServerState {
|
||||
.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)
|
||||
|
@ -338,5 +333,4 @@ struct SelectUserView: View {
|
|||
// selectUserAllServersSplashscreen = serverSelection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
E139CC1E28EC83E400688DE2 /* Int.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int.swift; sourceTree = "<group>"; };
|
||||
E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = "<group>"; };
|
||||
E13D98EC2D0664C1005FE96D /* NotificationSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSet.swift; sourceTree = "<group>"; };
|
||||
E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
E13DD3C727164B1E009D4DAF /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = "<group>"; };
|
||||
E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1605,7 +1608,7 @@
|
|||
E1549656296CA2EF00C4EF88 /* SwiftfinDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftfinDefaults.swift; sourceTree = "<group>"; };
|
||||
E1549657296CA2EF00C4EF88 /* UserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
|
||||
E1549658296CA2EF00C4EF88 /* SwiftfinStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = "<group>"; };
|
||||
E1549659296CA2EF00C4EF88 /* SwiftfinNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftfinNotifications.swift; sourceTree = "<group>"; };
|
||||
E1549659296CA2EF00C4EF88 /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
|
||||
E154965B296CA2EF00C4EF88 /* DownloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
|
||||
E154965D296CA2EF00C4EF88 /* LogManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; };
|
||||
E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineEnumToggle.swift; sourceTree = "<group>"; };
|
||||
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -115,7 +115,7 @@ struct AddServerUserView: View {
|
|||
UIDevice.feedback(.success)
|
||||
|
||||
router.dismissCoordinator {
|
||||
Notifications[.didAddServerUser].post(object: newUser)
|
||||
Notifications[.didAddServerUser].post(newUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -566,22 +566,17 @@ struct SelectUserView: View {
|
|||
Notifications[.didSignIn].post()
|
||||
}
|
||||
}
|
||||
.onNotification(.didConnectToServer) { notification in
|
||||
if let server = notification.object as? ServerState {
|
||||
.onNotification(.didConnectToServer) { server in
|
||||
viewModel.send(.getServers)
|
||||
serverSelection = .server(id: server.id)
|
||||
}
|
||||
}
|
||||
.onNotification(.didChangeCurrentServerURL) { notification in
|
||||
if let server = notification.object as? ServerState {
|
||||
.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)
|
||||
|
@ -593,7 +588,6 @@ struct SelectUserView: View {
|
|||
// change splash screen selection if necessary
|
||||
selectUserAllServersSplashscreen = serverSelection
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
Text("Delete User"),
|
||||
isPresented: $isPresentingConfirmDeleteUsers,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue