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:
Ethan Pippin 2024-12-08 23:57:16 -07:00 committed by GitHub
parent e856303181
commit c8acd780be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 376 additions and 269 deletions

View File

@ -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
} }
} }

View File

@ -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)
}
} }
} }

View File

@ -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
) )
) )

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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)")
} }

View File

@ -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)

View File

@ -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
} }
} }

View File

@ -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)
} }
} }

View File

@ -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)
} }
} }
} }

View File

@ -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 .store(in: &cancellables)
await self?.send(.backgroundRefresh)
} Notifications[.itemMetadataDidChange]
} .publisher
} else if let newItem = notification.object as? BaseItemDto, newItem.id == self?.item.id { .sink { newItem in
Task { [weak self] in guard let newItemID = newItem.id, newItemID == self.item.id else { return }
await self?.send(.replace(newItem))
} 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 {

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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 }

View File

@ -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])

View File

@ -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

View File

@ -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 }

View File

@ -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(

View File

@ -310,33 +310,27 @@ 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) { notification in .onNotification(.didChangeCurrentServerURL) { 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(.didDeleteServer) { notification in .onNotification(.didDeleteServer) { server 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) } else {
} else { serverSelection = .all
serverSelection = .all
}
} }
// change splash screen selection if necessary
// selectUserAllServersSplashscreen = serverSelection
} }
// change splash screen selection if necessary
// selectUserAllServersSplashscreen = serverSelection
} }
} }
} }

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
} }
} }
} }

View File

@ -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)
} }

View File

@ -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)

View File

@ -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)
} }
} }
} }

View File

@ -566,33 +566,27 @@ 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) { notification in .onNotification(.didChangeCurrentServerURL) { 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(.didDeleteServer) { notification in .onNotification(.didDeleteServer) { server 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) } else {
} else { serverSelection = .all
serverSelection = .all
}
} }
// change splash screen selection if necessary
selectUserAllServersSplashscreen = serverSelection
} }
// change splash screen selection if necessary
selectUserAllServersSplashscreen = serverSelection
} }
.alert( .alert(
Text("Delete User"), Text("Delete User"),

View File

@ -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,