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
|
import SwiftUI
|
||||||
|
|
||||||
struct RedrawOnNotificationView<Content: View>: View {
|
struct RedrawOnNotificationView<Content: View, P>: View {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var id = 0
|
private var id = 0
|
||||||
|
|
||||||
private let name: NSNotification.Name
|
private let key: Notifications.Key<P>
|
||||||
private let content: () -> Content
|
private let content: () -> Content
|
||||||
|
|
||||||
init(name: NSNotification.Name, @ViewBuilder content: @escaping () -> Content) {
|
init(_ key: Notifications.Key<P>, @ViewBuilder content: @escaping () -> Content) {
|
||||||
self.name = name
|
self.key = key
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ swiftfinNotification: Notifications.Key, @ViewBuilder content: @escaping () -> Content) {
|
|
||||||
self.name = swiftfinNotification.underlyingNotification.name
|
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content()
|
content()
|
||||||
.id(id)
|
.id(id)
|
||||||
.onNotification(name) { _ in
|
.onNotification(key) { _ in
|
||||||
id += 1
|
id += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,13 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct OnReceiveNotificationModifier: ViewModifier {
|
struct OnReceiveNotificationModifier<P, K: Notifications.Key<P>>: ViewModifier {
|
||||||
|
|
||||||
let notification: NSNotification.Name
|
let key: K
|
||||||
let onReceive: (Notification) -> Void
|
let onReceive: (P) -> Void
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.onReceive(NotificationCenter.default.publisher(for: notification)) {
|
.onReceive(key.publisher, perform: onReceive)
|
||||||
onReceive($0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
modifier(
|
||||||
OnReceiveNotificationModifier(
|
OnReceiveNotificationModifier(
|
||||||
notification: name,
|
key: key,
|
||||||
onReceive: action
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func onNotification(_ swiftfinNotification: Notifications.Key, perform action: @escaping (Notification) -> Void) -> some View {
|
|
||||||
modifier(
|
|
||||||
OnReceiveNotificationModifier(
|
|
||||||
notification: swiftfinNotification.underlyingNotification.name,
|
|
||||||
onReceive: action
|
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
|
return editServer.state
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications[.didChangeCurrentServerURL].post(object: newState)
|
Notifications[.didChangeCurrentServerURL].post(newState)
|
||||||
} catch {
|
} catch {
|
||||||
logger.critical("\(error.localizedDescription)")
|
logger.critical("\(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ final class HomeViewModel: ViewModel, Stateful {
|
||||||
// TODO: replace with views checking what notifications were
|
// TODO: replace with views checking what notifications were
|
||||||
// posted since last disappear
|
// posted since last disappear
|
||||||
@Published
|
@Published
|
||||||
var notificationsReceived: Set<Notifications.Key> = []
|
var notificationsReceived: NotificationSet = .init()
|
||||||
|
|
||||||
private var backgroundRefreshTask: AnyCancellable?
|
private var backgroundRefreshTask: AnyCancellable?
|
||||||
private var refreshTask: AnyCancellable?
|
private var refreshTask: AnyCancellable?
|
||||||
|
@ -65,13 +65,14 @@ final class HomeViewModel: ViewModel, Stateful {
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
Notifications[.itemMetadataDidChange].publisher
|
Notifications[.itemMetadataDidChange]
|
||||||
|
.publisher
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// Necessary because when this notification is posted, even with asyncAfter,
|
// Necessary because when this notification is posted, even with asyncAfter,
|
||||||
// the view will cause layout issues since it will redraw while in landscape.
|
// the view will cause layout issues since it will redraw while in landscape.
|
||||||
// TODO: look for better solution
|
// TODO: look for better solution
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.notificationsReceived.insert(Notifications.Key.itemMetadataDidChange)
|
self.notificationsReceived.insert(.itemMetadataDidChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
|
@ -96,7 +96,7 @@ class DeleteItemViewModel: ViewModel, Stateful, Eventful {
|
||||||
// MARK: Metadata Refresh Logic
|
// MARK: Metadata Refresh Logic
|
||||||
|
|
||||||
private func deleteItem() async throws {
|
private func deleteItem() async throws {
|
||||||
guard let itemID = item?.id else {
|
guard let item, let itemID = item.id else {
|
||||||
throw JellyfinAPIError(L10n.unknownError)
|
throw JellyfinAPIError(L10n.unknownError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ class DeleteItemViewModel: ViewModel, Stateful, Eventful {
|
||||||
_ = try await userSession.client.send(request)
|
_ = try await userSession.client.send(request)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
Notifications[.didDeleteItem].post(object: item)
|
Notifications[.didDeleteItem].post(itemID)
|
||||||
self.item = nil
|
self.item = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,7 +237,7 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
|
||||||
try await refreshItem()
|
try await refreshItem()
|
||||||
|
|
||||||
await MainActor.run {
|
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.item = response.value
|
||||||
self.progress = 0.0
|
self.progress = 0.0
|
||||||
|
|
||||||
Notifications[.itemMetadataDidChange].post(object: item)
|
Notifications[.itemMetadataDidChange].post(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,23 +89,24 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
self.item = item
|
self.item = item
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// TODO: should replace with a more robust "PlaybackManager"
|
Notifications[.itemShouldRefresh]
|
||||||
Notifications[.itemMetadataDidChange].publisher
|
.publisher
|
||||||
.sink { [weak self] notification in
|
.sink { itemID, parentID in
|
||||||
if let userInfo = notification.object as? [String: String] {
|
guard itemID == self.item.id || parentID == self.item.id else { return }
|
||||||
if let itemID = userInfo["itemID"], itemID == item.id {
|
|
||||||
Task { [weak self] in
|
Task {
|
||||||
await self?.send(.backgroundRefresh)
|
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 {
|
.store(in: &cancellables)
|
||||||
Task { [weak self] in
|
|
||||||
await self?.send(.replace(newItem))
|
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)
|
.store(in: &cancellables)
|
||||||
|
@ -314,6 +315,8 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
|
|
||||||
private func setIsPlayed(_ isPlayed: Bool) async throws {
|
private func setIsPlayed(_ isPlayed: Bool) async throws {
|
||||||
|
|
||||||
|
guard let itemID = item.id else { return }
|
||||||
|
|
||||||
let request: Request<UserItemDataDto>
|
let request: Request<UserItemDataDto>
|
||||||
|
|
||||||
if isPlayed {
|
if isPlayed {
|
||||||
|
@ -329,9 +332,7 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = try await userSession.client.send(request)
|
let _ = try await userSession.client.send(request)
|
||||||
|
Notifications[.itemShouldRefresh].post((itemID, nil))
|
||||||
let ids = ["itemID": item.id]
|
|
||||||
Notifications[.itemMetadataDidChange].post(object: ids)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setIsFavorite(_ isFavorite: Bool) async throws {
|
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`.
|
// 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
|
// If we ever care for viewing seasons directly, subclass from that and have the library view model
|
||||||
// as a property.
|
// as a property.
|
||||||
final class SeasonItemViewModel: PagingLibraryViewModel<BaseItemDto> {
|
final class SeasonItemViewModel: PagingLibraryViewModel<BaseItemDto>, Identifiable {
|
||||||
|
|
||||||
let season: BaseItemDto
|
let season: BaseItemDto
|
||||||
|
|
||||||
|
var id: String? {
|
||||||
|
season.id
|
||||||
|
}
|
||||||
|
|
||||||
init(season: BaseItemDto) {
|
init(season: BaseItemDto) {
|
||||||
self.season = season
|
self.season = season
|
||||||
super.init(parent: season)
|
super.init(parent: season)
|
||||||
|
@ -43,14 +47,3 @@ final class SeasonItemViewModel: PagingLibraryViewModel<BaseItemDto> {
|
||||||
return response.value.items ?? []
|
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 Defaults
|
||||||
import Factory
|
import Factory
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import IdentifiedCollections
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import OrderedCollections
|
|
||||||
|
|
||||||
// TODO: care for one long episodes list?
|
// TODO: care for one long episodes list?
|
||||||
// - after SeasonItemViewModel is bidirectional
|
// - after SeasonItemViewModel is bidirectional
|
||||||
|
@ -19,7 +19,7 @@ import OrderedCollections
|
||||||
final class SeriesItemViewModel: ItemViewModel {
|
final class SeriesItemViewModel: ItemViewModel {
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var seasons: OrderedSet<SeasonItemViewModel> = []
|
var seasons: IdentifiedArrayOf<SeasonItemViewModel> = []
|
||||||
|
|
||||||
override func onRefresh() async throws {
|
override func onRefresh() async throws {
|
||||||
|
|
||||||
|
|
|
@ -67,8 +67,9 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventfu
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
final var backgroundStates: OrderedSet<BackgroundState> = []
|
final var backgroundStates: OrderedSet<BackgroundState> = []
|
||||||
|
/// - Keys: the `hashValue` of the `Element.ID`
|
||||||
@Published
|
@Published
|
||||||
final var elements: IdentifiedArrayOf<Element>
|
final var elements: IdentifiedArray<Int, Element>
|
||||||
@Published
|
@Published
|
||||||
final var state: State = .initial
|
final var state: State = .initial
|
||||||
@Published
|
@Published
|
||||||
|
@ -104,7 +105,7 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventfu
|
||||||
parent: (any LibraryParent)? = nil
|
parent: (any LibraryParent)? = nil
|
||||||
) {
|
) {
|
||||||
self.filterViewModel = nil
|
self.filterViewModel = nil
|
||||||
self.elements = IdentifiedArray(uniqueElements: data)
|
self.elements = IdentifiedArray(uniqueElements: data, id: \.id.hashValue)
|
||||||
self.isStatic = true
|
self.isStatic = true
|
||||||
self.hasNextPage = false
|
self.hasNextPage = false
|
||||||
self.pageSize = DefaultPageSize
|
self.pageSize = DefaultPageSize
|
||||||
|
@ -131,7 +132,7 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventfu
|
||||||
filters: ItemFilterCollection? = nil,
|
filters: ItemFilterCollection? = nil,
|
||||||
pageSize: Int = DefaultPageSize
|
pageSize: Int = DefaultPageSize
|
||||||
) {
|
) {
|
||||||
self.elements = IdentifiedArray()
|
self.elements = IdentifiedArray(id: \.id.hashValue)
|
||||||
self.isStatic = false
|
self.isStatic = false
|
||||||
self.pageSize = pageSize
|
self.pageSize = pageSize
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
@ -160,10 +161,10 @@ class PagingLibraryViewModel<Element: Poster & Identifiable>: ViewModel, Eventfu
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
Notifications[.didDeleteItem].publisher
|
Notifications[.didDeleteItem]
|
||||||
.sink(receiveCompletion: { _ in }) { [weak self] notification in
|
.publisher
|
||||||
guard let item = notification.object as? Element else { return }
|
.sink { id in
|
||||||
self?.elements.remove(item)
|
self.elements.remove(id: id.hashValue)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ class ServerConnectionViewModel: ViewModel {
|
||||||
UserDefaults.userSuite(id: user.id).removeAll()
|
UserDefaults.userSuite(id: user.id).removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications[.didDeleteServer].post(object: server)
|
Notifications[.didDeleteServer].post(server)
|
||||||
} catch {
|
} catch {
|
||||||
logger.critical("Unable to delete server: \(server.name)")
|
logger.critical("Unable to delete server: \(server.name)")
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ class ServerConnectionViewModel: ViewModel {
|
||||||
return storedServer.state
|
return storedServer.state
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications[.didChangeCurrentServerURL].post(object: newState)
|
Notifications[.didChangeCurrentServerURL].post(newState)
|
||||||
|
|
||||||
self.server = newState
|
self.server = newState
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -213,8 +213,10 @@ class VideoPlayerManager: ViewModel {
|
||||||
|
|
||||||
func sendStopReport() {
|
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]
|
let ids = ["itemID": currentViewModel.item.id, "seriesID": currentViewModel.item.parentID]
|
||||||
Notifications[.itemMetadataDidChange].post(object: ids)
|
// Notifications[.itemMetadataDidChange].post(ids)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
guard Defaults[.sendProgressReports] else { return }
|
guard Defaults[.sendProgressReports] else { return }
|
||||||
|
|
|
@ -58,10 +58,10 @@ struct SwiftfinApp: App {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainCoordinator()
|
MainCoordinator()
|
||||||
.view()
|
.view()
|
||||||
.onNotification(UIApplication.didEnterBackgroundNotification) { _ in
|
.onNotification(.applicationDidEnterBackground) {
|
||||||
Defaults[.backgroundTimeStamp] = Date.now
|
Defaults[.backgroundTimeStamp] = Date.now
|
||||||
}
|
}
|
||||||
.onNotification(UIApplication.willEnterForegroundNotification) { _ in
|
.onNotification(.applicationWillEnterForeground) {
|
||||||
// TODO: needs to check if any background playback is happening
|
// TODO: needs to check if any background playback is happening
|
||||||
let backgroundedInterval = Date.now.timeIntervalSince(Defaults[.backgroundTimeStamp])
|
let backgroundedInterval = Date.now.timeIntervalSince(Defaults[.backgroundTimeStamp])
|
||||||
|
|
||||||
|
|
|
@ -153,7 +153,7 @@ struct ConnectToServerView: View {
|
||||||
.onReceive(viewModel.events) { event in
|
.onReceive(viewModel.events) { event in
|
||||||
switch event {
|
switch event {
|
||||||
case let .connected(server):
|
case let .connected(server):
|
||||||
Notifications[.didConnectToServer].post(object: server)
|
Notifications[.didConnectToServer].post(server)
|
||||||
router.popLast()
|
router.popLast()
|
||||||
case let .duplicateServer(server):
|
case let .duplicateServer(server):
|
||||||
duplicateServer = server
|
duplicateServer = server
|
||||||
|
|
|
@ -79,8 +79,8 @@ extension SeriesEpisodeSelector {
|
||||||
onContentFocus: { focusedEpisodeID = lastFocusedEpisodeID },
|
onContentFocus: { focusedEpisodeID = lastFocusedEpisodeID },
|
||||||
top: "seasons"
|
top: "seasons"
|
||||||
)
|
)
|
||||||
.onChange(of: viewModel) { _, newValue in
|
.onChange(of: viewModel.id) {
|
||||||
lastFocusedEpisodeID = newValue.elements.first?.id
|
lastFocusedEpisodeID = viewModel.elements.first?.id
|
||||||
}
|
}
|
||||||
.onChange(of: focusedEpisodeID) { _, newValue in
|
.onChange(of: focusedEpisodeID) { _, newValue in
|
||||||
guard let newValue else { return }
|
guard let newValue else { return }
|
||||||
|
|
|
@ -21,15 +21,19 @@ struct SeriesEpisodeSelector: View {
|
||||||
@State
|
@State
|
||||||
private var didSelectPlayButtonSeason = false
|
private var didSelectPlayButtonSeason = false
|
||||||
@State
|
@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 {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SeasonsHStack(viewModel: viewModel, selection: $selection)
|
SeasonsHStack(viewModel: viewModel, selection: $selection)
|
||||||
.environmentObject(parentFocusGuide)
|
.environmentObject(parentFocusGuide)
|
||||||
|
|
||||||
if let selection {
|
if let selectionViewModel {
|
||||||
EpisodeHStack(viewModel: selection, playButtonItem: viewModel.playButtonItem)
|
EpisodeHStack(viewModel: selectionViewModel, playButtonItem: viewModel.playButtonItem)
|
||||||
.environmentObject(parentFocusGuide)
|
.environmentObject(parentFocusGuide)
|
||||||
} else {
|
} else {
|
||||||
LoadingHStack()
|
LoadingHStack()
|
||||||
|
@ -40,17 +44,17 @@ struct SeriesEpisodeSelector: View {
|
||||||
guard !didSelectPlayButtonSeason else { return }
|
guard !didSelectPlayButtonSeason else { return }
|
||||||
didSelectPlayButtonSeason = true
|
didSelectPlayButtonSeason = true
|
||||||
|
|
||||||
if let season = viewModel.seasons.first(where: { $0.season.id == newValue.seasonID }) {
|
if let playButtonSeason = viewModel.seasons.first(where: { $0.id == newValue.seasonID }) {
|
||||||
selection = season
|
selection = playButtonSeason.id
|
||||||
} else {
|
} else {
|
||||||
selection = viewModel.seasons.first
|
selection = viewModel.seasons.first?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: selection) { _, newValue in
|
.onChange(of: selection) { _ in
|
||||||
guard let newValue else { return }
|
guard let selectionViewModel else { return }
|
||||||
|
|
||||||
if newValue.state == .initial {
|
if selectionViewModel.state == .initial {
|
||||||
newValue.send(.refresh)
|
selectionViewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,31 +70,31 @@ extension SeriesEpisodeSelector {
|
||||||
private var focusGuide: FocusGuide
|
private var focusGuide: FocusGuide
|
||||||
|
|
||||||
@FocusState
|
@FocusState
|
||||||
private var focusedSeason: SeasonItemViewModel?
|
private var focusedSeason: SeasonItemViewModel.ID?
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: SeriesItemViewModel
|
var viewModel: SeriesItemViewModel
|
||||||
|
|
||||||
var selection: Binding<SeasonItemViewModel?>
|
var selection: Binding<SeasonItemViewModel.ID?>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(viewModel.seasons, id: \.season.id) { seasonViewModel in
|
ForEach(viewModel.seasons) { seasonViewModel in
|
||||||
Button {
|
Button {
|
||||||
Text(seasonViewModel.season.displayTitle)
|
Text(seasonViewModel.season.displayTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.if(selection.wrappedValue == seasonViewModel) { text in
|
.if(selection.wrappedValue == seasonViewModel.id) { text in
|
||||||
text
|
text
|
||||||
.background(Color.white)
|
.background(Color.white)
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.card)
|
.buttonStyle(.card)
|
||||||
.focused($focusedSeason, equals: seasonViewModel)
|
.focused($focusedSeason, equals: seasonViewModel.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.focusGuide(
|
.focusGuide(
|
||||||
|
|
|
@ -310,22 +310,17 @@ struct SelectUserView: View {
|
||||||
Notifications[.didSignIn].post()
|
Notifications[.didSignIn].post()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onNotification(.didConnectToServer) { notification in
|
.onNotification(.didConnectToServer) { server in
|
||||||
if let server = notification.object as? ServerState {
|
|
||||||
viewModel.send(.getServers)
|
viewModel.send(.getServers)
|
||||||
serverSelection = .server(id: server.id)
|
serverSelection = .server(id: server.id)
|
||||||
}
|
}
|
||||||
}
|
.onNotification(.didChangeCurrentServerURL) { server in
|
||||||
.onNotification(.didChangeCurrentServerURL) { notification in
|
|
||||||
if let server = notification.object as? ServerState {
|
|
||||||
viewModel.send(.getServers)
|
viewModel.send(.getServers)
|
||||||
serverSelection = .server(id: server.id)
|
serverSelection = .server(id: server.id)
|
||||||
}
|
}
|
||||||
}
|
.onNotification(.didDeleteServer) { server in
|
||||||
.onNotification(.didDeleteServer) { notification in
|
|
||||||
viewModel.send(.getServers)
|
viewModel.send(.getServers)
|
||||||
|
|
||||||
if let server = notification.object as? ServerState {
|
|
||||||
if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id {
|
if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id {
|
||||||
if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first {
|
if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first {
|
||||||
serverSelection = .server(id: first.id)
|
serverSelection = .server(id: first.id)
|
||||||
|
@ -339,4 +334,3 @@ struct SelectUserView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -545,6 +545,8 @@
|
||||||
E13AF3B828A0C598009093AB /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B728A0C598009093AB /* NukeExtensions */; };
|
E13AF3B828A0C598009093AB /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B728A0C598009093AB /* NukeExtensions */; };
|
||||||
E13AF3BA28A0C598009093AB /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B928A0C598009093AB /* NukeUI */; };
|
E13AF3BA28A0C598009093AB /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B928A0C598009093AB /* NukeUI */; };
|
||||||
E13AF3BC28A0C59E009093AB /* BlurHashKit in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3BB28A0C59E009093AB /* BlurHashKit */; };
|
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 */; };
|
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; };
|
||||||
E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3C52716499E009D4DAF /* CoreStore */; };
|
E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3C52716499E009D4DAF /* CoreStore */; };
|
||||||
E13DD3C827164B1E009D4DAF /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDevice.swift */; };
|
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 */; };
|
E1549663296CA2EF00C4EF88 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549657296CA2EF00C4EF88 /* UserSession.swift */; };
|
||||||
E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549658296CA2EF00C4EF88 /* SwiftfinStore.swift */; };
|
E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549658296CA2EF00C4EF88 /* SwiftfinStore.swift */; };
|
||||||
E1549665296CA2EF00C4EF88 /* 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 */; };
|
E1549666296CA2EF00C4EF88 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549659296CA2EF00C4EF88 /* Notifications.swift */; };
|
||||||
E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549659296CA2EF00C4EF88 /* SwiftfinNotifications.swift */; };
|
E1549667296CA2EF00C4EF88 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1549659296CA2EF00C4EF88 /* Notifications.swift */; };
|
||||||
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965B296CA2EF00C4EF88 /* DownloadManager.swift */; };
|
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154965B296CA2EF00C4EF88 /* DownloadManager.swift */; };
|
||||||
E154966B296CA2EF00C4EF88 /* 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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineEnumToggle.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2777,6 +2780,7 @@
|
||||||
E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */,
|
E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */,
|
||||||
E1DE2B4E2B983F3200F6715F /* LibraryParent */,
|
E1DE2B4E2B983F3200F6715F /* LibraryParent */,
|
||||||
4E2AC4C02C6C48EB00DD600D /* MediaComponents */,
|
4E2AC4C02C6C48EB00DD600D /* MediaComponents */,
|
||||||
|
E13D98EC2D0664C1005FE96D /* NotificationSet.swift */,
|
||||||
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
||||||
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
|
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
|
||||||
4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */,
|
4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */,
|
||||||
|
@ -3747,8 +3751,8 @@
|
||||||
E1549655296CA2EF00C4EF88 /* DownloadTask.swift */,
|
E1549655296CA2EF00C4EF88 /* DownloadTask.swift */,
|
||||||
E19D41A92BF077130082B8B2 /* Keychain.swift */,
|
E19D41A92BF077130082B8B2 /* Keychain.swift */,
|
||||||
E154965D296CA2EF00C4EF88 /* LogManager.swift */,
|
E154965D296CA2EF00C4EF88 /* LogManager.swift */,
|
||||||
|
E1549659296CA2EF00C4EF88 /* Notifications.swift */,
|
||||||
E1549656296CA2EF00C4EF88 /* SwiftfinDefaults.swift */,
|
E1549656296CA2EF00C4EF88 /* SwiftfinDefaults.swift */,
|
||||||
E1549659296CA2EF00C4EF88 /* SwiftfinNotifications.swift */,
|
|
||||||
E1549657296CA2EF00C4EF88 /* UserSession.swift */,
|
E1549657296CA2EF00C4EF88 /* UserSession.swift */,
|
||||||
);
|
);
|
||||||
path = Services;
|
path = Services;
|
||||||
|
@ -4951,6 +4955,7 @@
|
||||||
E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */,
|
E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */,
|
||||||
E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
|
||||||
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
|
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
|
||||||
|
E13D98EE2D0664C1005FE96D /* NotificationSet.swift in Sources */,
|
||||||
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
|
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
|
||||||
4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */,
|
4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */,
|
||||||
E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
|
E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
|
||||||
|
@ -5070,7 +5075,7 @@
|
||||||
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */,
|
4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */,
|
||||||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||||
E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
|
E1549667296CA2EF00C4EF88 /* Notifications.swift in Sources */,
|
||||||
E150C0BB2BFD44F500944FFA /* ImagePipeline.swift in Sources */,
|
E150C0BB2BFD44F500944FFA /* ImagePipeline.swift in Sources */,
|
||||||
E11E0E8D2BF7E76F007676DD /* DataCache.swift in Sources */,
|
E11E0E8D2BF7E76F007676DD /* DataCache.swift in Sources */,
|
||||||
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */,
|
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */,
|
||||||
|
@ -5326,6 +5331,7 @@
|
||||||
E17FB55528C1250B00311DFE /* SimilarItemsHStack.swift in Sources */,
|
E17FB55528C1250B00311DFE /* SimilarItemsHStack.swift in Sources */,
|
||||||
C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */,
|
C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */,
|
||||||
5364F455266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */,
|
5364F455266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */,
|
||||||
|
E13D98ED2D0664C1005FE96D /* NotificationSet.swift in Sources */,
|
||||||
E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
|
E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
|
||||||
4E5071D72CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */,
|
4E5071D72CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */,
|
||||||
E1B33ECF28EB6EA90073B0FD /* OverlayMenu.swift in Sources */,
|
E1B33ECF28EB6EA90073B0FD /* OverlayMenu.swift in Sources */,
|
||||||
|
@ -5622,7 +5628,7 @@
|
||||||
E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
|
E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
|
||||||
E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */,
|
E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */,
|
||||||
E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */,
|
E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */,
|
||||||
E1549666296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
|
E1549666296CA2EF00C4EF88 /* Notifications.swift in Sources */,
|
||||||
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */,
|
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */,
|
||||||
E1A1528528FD191A00600579 /* TextPair.swift in Sources */,
|
E1A1528528FD191A00600579 /* TextPair.swift in Sources */,
|
||||||
6334175D287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift in Sources */,
|
6334175D287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift in Sources */,
|
||||||
|
@ -6228,7 +6234,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 78;
|
CURRENT_PROJECT_VERSION = 78;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
@ -6244,7 +6250,7 @@
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
OTHER_CFLAGS = "";
|
OTHER_CFLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin;
|
PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
@ -6268,7 +6274,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 78;
|
CURRENT_PROJECT_VERSION = 78;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
@ -6284,7 +6290,7 @@
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
OTHER_CFLAGS = "";
|
OTHER_CFLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin;
|
PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
|
|
@ -97,10 +97,10 @@ struct SwiftfinApp: App {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
versionedView
|
versionedView
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onNotification(UIApplication.didEnterBackgroundNotification) { _ in
|
.onNotification(.applicationDidEnterBackground) {
|
||||||
Defaults[.backgroundTimeStamp] = Date.now
|
Defaults[.backgroundTimeStamp] = Date.now
|
||||||
}
|
}
|
||||||
.onNotification(UIApplication.willEnterForegroundNotification) { _ in
|
.onNotification(.applicationWillEnterForeground) {
|
||||||
|
|
||||||
// TODO: needs to check if any background playback is happening
|
// TODO: needs to check if any background playback is happening
|
||||||
// - atow, background video playback isn't officially supported
|
// - atow, background video playback isn't officially supported
|
||||||
|
|
|
@ -55,15 +55,21 @@ extension View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func onAppDidEnterBackground(_ action: @escaping () -> Void) -> some View {
|
func onAppDidEnterBackground(_ action: @escaping () -> Void) -> some View {
|
||||||
onNotification(UIApplication.didEnterBackgroundNotification, perform: { _ in action() })
|
onNotification(.applicationDidEnterBackground) {
|
||||||
|
action()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func onAppWillResignActive(_ action: @escaping () -> Void) -> some View {
|
func onAppWillResignActive(_ action: @escaping () -> Void) -> some View {
|
||||||
onNotification(UIApplication.willResignActiveNotification, perform: { _ in action() })
|
onNotification(.applicationWillResignActive) { _ in
|
||||||
|
action()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func onAppWillTerminate(_ action: @escaping () -> Void) -> some View {
|
func onAppWillTerminate(_ action: @escaping () -> Void) -> some View {
|
||||||
onNotification(UIApplication.willTerminateNotification, perform: { _ in action() })
|
onNotification(.applicationWillTerminate) { _ in
|
||||||
|
action()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -81,7 +81,8 @@ extension AppURLHandler {
|
||||||
// It would be nice if the ItemViewModel could be initialized to id later.
|
// It would be nice if the ItemViewModel could be initialized to id later.
|
||||||
getItem(userID: userID, itemID: itemID) { item in
|
getItem(userID: userID, itemID: itemID) { item in
|
||||||
guard let item = item else { return }
|
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
|
return true
|
||||||
|
|
|
@ -115,7 +115,7 @@ struct AddServerUserView: View {
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
|
|
||||||
router.dismissCoordinator {
|
router.dismissCoordinator {
|
||||||
Notifications[.didAddServerUser].post(object: newUser)
|
Notifications[.didAddServerUser].post(newUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,8 +147,7 @@ struct ServerUsersView: View {
|
||||||
} message: {
|
} message: {
|
||||||
Text(L10n.deleteUserSelfDeletion(viewModel.userSession.user.username))
|
Text(L10n.deleteUserSelfDeletion(viewModel.userSession.user.username))
|
||||||
}
|
}
|
||||||
.onNotification(.didAddServerUser) { notification in
|
.onNotification(.didAddServerUser) { newUser in
|
||||||
let newUser = notification.object as! UserDto
|
|
||||||
viewModel.send(.appendUser(newUser))
|
viewModel.send(.appendUser(newUser))
|
||||||
router.route(to: \.userDetails, newUser)
|
router.route(to: \.userDetails, newUser)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ struct ConnectToServerView: View {
|
||||||
case let .connected(server):
|
case let .connected(server):
|
||||||
UIDevice.feedback(.success)
|
UIDevice.feedback(.success)
|
||||||
|
|
||||||
Notifications[.didConnectToServer].post(object: server)
|
Notifications[.didConnectToServer].post(server)
|
||||||
router.popLast()
|
router.popLast()
|
||||||
case let .duplicateServer(server):
|
case let .duplicateServer(server):
|
||||||
UIDevice.feedback(.warning)
|
UIDevice.feedback(.warning)
|
||||||
|
|
|
@ -20,16 +20,20 @@ struct SeriesEpisodeSelector: View {
|
||||||
@State
|
@State
|
||||||
private var didSelectPlayButtonSeason = false
|
private var didSelectPlayButtonSeason = false
|
||||||
@State
|
@State
|
||||||
private var selection: SeasonItemViewModel?
|
private var selection: SeasonItemViewModel.ID?
|
||||||
|
|
||||||
|
private var selectionViewModel: SeasonItemViewModel? {
|
||||||
|
viewModel.seasons.first(where: { $0.id == selection })
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var seasonSelectorMenu: some View {
|
private var seasonSelectorMenu: some View {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(viewModel.seasons, id: \.season.id) { seasonViewModel in
|
ForEach(viewModel.seasons, id: \.season.id) { seasonViewModel in
|
||||||
Button {
|
Button {
|
||||||
selection = seasonViewModel
|
selection = seasonViewModel.id
|
||||||
} label: {
|
} label: {
|
||||||
if seasonViewModel == selection {
|
if seasonViewModel.id == selection {
|
||||||
Label(seasonViewModel.season.displayTitle, systemImage: "checkmark")
|
Label(seasonViewModel.season.displayTitle, systemImage: "checkmark")
|
||||||
} else {
|
} else {
|
||||||
Text(seasonViewModel.season.displayTitle)
|
Text(seasonViewModel.season.displayTitle)
|
||||||
|
@ -38,7 +42,7 @@ struct SeriesEpisodeSelector: View {
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Label(
|
||||||
selection?.season.displayTitle ?? .emptyDash,
|
selectionViewModel?.season.displayTitle ?? .emptyDash,
|
||||||
systemImage: "chevron.down"
|
systemImage: "chevron.down"
|
||||||
)
|
)
|
||||||
.labelStyle(.episodeSelector)
|
.labelStyle(.episodeSelector)
|
||||||
|
@ -52,8 +56,8 @@ struct SeriesEpisodeSelector: View {
|
||||||
.edgePadding([.bottom, .horizontal])
|
.edgePadding([.bottom, .horizontal])
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
if let selection {
|
if let selectionViewModel {
|
||||||
EpisodeHStack(viewModel: selection, playButtonItem: viewModel.playButtonItem)
|
EpisodeHStack(viewModel: selectionViewModel, playButtonItem: viewModel.playButtonItem)
|
||||||
} else {
|
} else {
|
||||||
LoadingHStack()
|
LoadingHStack()
|
||||||
}
|
}
|
||||||
|
@ -65,17 +69,17 @@ struct SeriesEpisodeSelector: View {
|
||||||
guard !didSelectPlayButtonSeason else { return }
|
guard !didSelectPlayButtonSeason else { return }
|
||||||
didSelectPlayButtonSeason = true
|
didSelectPlayButtonSeason = true
|
||||||
|
|
||||||
if let season = viewModel.seasons.first(where: { $0.season.id == newValue.seasonID }) {
|
if let playButtonSeason = viewModel.seasons.first(where: { $0.id == newValue.seasonID }) {
|
||||||
selection = season
|
selection = playButtonSeason.id
|
||||||
} else {
|
} else {
|
||||||
selection = viewModel.seasons.first
|
selection = viewModel.seasons.first?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: selection) { newValue in
|
.onChange(of: selection) { _ in
|
||||||
guard let newValue else { return }
|
guard let selectionViewModel else { return }
|
||||||
|
|
||||||
if newValue.state == .initial {
|
if selectionViewModel.state == .initial {
|
||||||
newValue.send(.refresh)
|
selectionViewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -566,22 +566,17 @@ struct SelectUserView: View {
|
||||||
Notifications[.didSignIn].post()
|
Notifications[.didSignIn].post()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onNotification(.didConnectToServer) { notification in
|
.onNotification(.didConnectToServer) { server in
|
||||||
if let server = notification.object as? ServerState {
|
|
||||||
viewModel.send(.getServers)
|
viewModel.send(.getServers)
|
||||||
serverSelection = .server(id: server.id)
|
serverSelection = .server(id: server.id)
|
||||||
}
|
}
|
||||||
}
|
.onNotification(.didChangeCurrentServerURL) { server in
|
||||||
.onNotification(.didChangeCurrentServerURL) { notification in
|
|
||||||
if let server = notification.object as? ServerState {
|
|
||||||
viewModel.send(.getServers)
|
viewModel.send(.getServers)
|
||||||
serverSelection = .server(id: server.id)
|
serverSelection = .server(id: server.id)
|
||||||
}
|
}
|
||||||
}
|
.onNotification(.didDeleteServer) { server in
|
||||||
.onNotification(.didDeleteServer) { notification in
|
|
||||||
viewModel.send(.getServers)
|
viewModel.send(.getServers)
|
||||||
|
|
||||||
if let server = notification.object as? ServerState {
|
|
||||||
if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id {
|
if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id {
|
||||||
if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first {
|
if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first {
|
||||||
serverSelection = .server(id: first.id)
|
serverSelection = .server(id: first.id)
|
||||||
|
@ -593,7 +588,6 @@ struct SelectUserView: View {
|
||||||
// change splash screen selection if necessary
|
// change splash screen selection if necessary
|
||||||
selectUserAllServersSplashscreen = serverSelection
|
selectUserAllServersSplashscreen = serverSelection
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.alert(
|
.alert(
|
||||||
Text("Delete User"),
|
Text("Delete User"),
|
||||||
isPresented: $isPresentingConfirmDeleteUsers,
|
isPresented: $isPresentingConfirmDeleteUsers,
|
||||||
|
|
|
@ -28,7 +28,7 @@ struct UserProfileSettingsView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var imageView: some View {
|
private var imageView: some View {
|
||||||
RedrawOnNotificationView(name: .init("didChangeUserProfileImage")) {
|
RedrawOnNotificationView(.didChangeUserProfileImage) {
|
||||||
ImageView(
|
ImageView(
|
||||||
viewModel.userSession.user.profileImageSource(
|
viewModel.userSession.user.profileImageSource(
|
||||||
client: viewModel.userSession.client,
|
client: viewModel.userSession.client,
|
||||||
|
|
Loading…
Reference in New Issue