This commit is contained in:
Ethan Pippin 2022-03-18 22:05:08 -06:00
parent 2d5f1a2c19
commit a467f0cbd7
17 changed files with 244 additions and 243 deletions

View File

@ -45,10 +45,10 @@ final class MainCoordinator: NavigationCoordinatable {
barAppearance.tintColor = UIColor(Color.jellyfinPurple) barAppearance.tintColor = UIColor(Color.jellyfinPurple)
// Notification setup for state // Notification setup for state
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn)) Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut)) Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
Notifications[.processDeepLink].subscribe(self, selector: #selector(processDeepLink(_:))) Notifications[.processDeepLink].subscribe(self, selector: #selector(processDeepLink(_:)))
Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeServerCurrentURI(_:))) Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeServerCurrentURI(_:)))
Defaults.publisher(.appAppearance) Defaults.publisher(.appAppearance)
.sink { _ in .sink { _ in

View File

@ -240,7 +240,7 @@ final class SessionManager {
Defaults[.lastServerUserID] = user.id Defaults[.lastServerUserID] = user.id
currentLogin = (server: currentServer.state, user: currentUser.state) currentLogin = (server: currentServer.state, user: currentUser.state)
Notifications[.didSignIn].post() Notifications[.didSignIn].post()
}) })
.map { _, user, _ in .map { _, user, _ in
user.state user.state
@ -255,7 +255,7 @@ final class SessionManager {
Defaults[.lastServerUserID] = user.id Defaults[.lastServerUserID] = user.id
setAuthHeader(with: user.accessToken) setAuthHeader(with: user.accessToken)
currentLogin = (server: server, user: user) currentLogin = (server: server, user: user)
Notifications[.didSignIn].post() Notifications[.didSignIn].post()
} }
// MARK: logout // MARK: logout
@ -265,7 +265,7 @@ final class SessionManager {
JellyfinAPI.basePath = "" JellyfinAPI.basePath = ""
setAuthHeader(with: "") setAuthHeader(with: "")
Defaults[.lastServerUserID] = nil Defaults[.lastServerUserID] = nil
Notifications[.didSignOut].post() Notifications[.didSignOut].post()
} }
// MARK: purge // MARK: purge
@ -278,7 +278,7 @@ final class SessionManager {
delete(server: server) delete(server: server)
} }
Notifications[.didPurge].post() Notifications[.didPurge].post()
} }
// MARK: delete user // MARK: delete user

View File

@ -10,61 +10,61 @@ import Foundation
class SwiftfinNotification { class SwiftfinNotification {
private let notificationName: Notification.Name private let notificationName: Notification.Name
fileprivate init(_ notificationName: Notification.Name) { fileprivate init(_ notificationName: Notification.Name) {
self.notificationName = notificationName self.notificationName = notificationName
} }
func post(object: Any? = nil) { func post(object: Any? = nil) {
Notifications.main.post(name: notificationName, object: object) Notifications.main.post(name: notificationName, object: object)
} }
func subscribe(_ observer: Any, selector: Selector) { func subscribe(_ observer: Any, selector: Selector) {
Notifications.main.addObserver(observer, selector: selector, name: notificationName, object: nil) Notifications.main.addObserver(observer, selector: selector, name: notificationName, object: nil)
} }
func unsubscribe(_ observer: Any) { func unsubscribe(_ observer: Any) {
Notifications.main.removeObserver(self, name: notificationName, object: nil) Notifications.main.removeObserver(self, name: notificationName, object: nil)
} }
} }
enum Notifications { enum Notifications {
static let main: NotificationCenter = { static let main: NotificationCenter = {
NotificationCenter() NotificationCenter()
}() }()
final class Key { final class Key {
public typealias NotificationKey = Notifications.Key public typealias NotificationKey = Notifications.Key
public let key: String public let key: String
public let underlyingNotification: SwiftfinNotification public let underlyingNotification: SwiftfinNotification
public init(_ key: String) { public init(_ key: String) {
self.key = key self.key = key
self.underlyingNotification = SwiftfinNotification(Notification.Name(key)) self.underlyingNotification = SwiftfinNotification(Notification.Name(key))
} }
} }
static subscript(key: Key) -> SwiftfinNotification { static subscript(key: Key) -> SwiftfinNotification {
return key.underlyingNotification key.underlyingNotification
} }
static func unsubscribe(_ observer: Any) { static func unsubscribe(_ observer: Any) {
main.removeObserver(observer) main.removeObserver(observer)
} }
} }
extension Notifications.Key { extension Notifications.Key {
static let didSignIn = NotificationKey("didSignIn") static let didSignIn = NotificationKey("didSignIn")
static let didSignOut = NotificationKey("didSignOut") static let didSignOut = NotificationKey("didSignOut")
static let processDeepLink = NotificationKey("processDeepLink") static let processDeepLink = NotificationKey("processDeepLink")
static let didPurge = NotificationKey("didPurge") static let didPurge = NotificationKey("didPurge")
static let didChangeServerCurrentURI = NotificationKey("didChangeCurrentLoginURI") static let didChangeServerCurrentURI = NotificationKey("didChangeCurrentLoginURI")
static let toggleOfflineMode = NotificationKey("toggleOfflineMode") static let toggleOfflineMode = NotificationKey("toggleOfflineMode")
static let didDeleteOfflineItem = NotificationKey("didDeleteOfflineItem") static let didDeleteOfflineItem = NotificationKey("didDeleteOfflineItem")
static let didAddDownload = NotificationKey("didAddDownload") static let didAddDownload = NotificationKey("didAddDownload")
static let didSendStopReport = NotificationKey("didSendStopReport") static let didSendStopReport = NotificationKey("didSendStopReport")
} }

View File

@ -34,8 +34,8 @@ final class HomeViewModel: ViewModel {
// Nov. 6, 2021 // Nov. 6, 2021
// This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing. // This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing.
// See ServerDetailViewModel.swift for feature request issue // See ServerDetailViewModel.swift for feature request issue
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn)) Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut)) Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
} }
@objc @objc

View File

@ -55,7 +55,7 @@ class ItemViewModel: ViewModel {
getSimilarItems() getSimilarItems()
Notifications[.didSendStopReport].subscribe(self, selector: #selector(receivedStopReport(_:))) Notifications[.didSendStopReport].subscribe(self, selector: #selector(receivedStopReport(_:)))
refreshItemVideoPlayerViewModel(for: item) refreshItemVideoPlayerViewModel(for: item)
} }
@ -69,7 +69,7 @@ class ItemViewModel: ViewModel {
} else { } else {
// Remove if necessary. Note that this cannot be in deinit as // Remove if necessary. Note that this cannot be in deinit as
// holding as an observer won't allow the object to be deinit-ed // holding as an observer won't allow the object to be deinit-ed
Notifications.unsubscribe(self) Notifications.unsubscribe(self)
} }
} }

View File

@ -25,7 +25,7 @@ class ServerDetailViewModel: ViewModel {
} receiveValue: { newServerState in } receiveValue: { newServerState in
self.server = newServerState self.server = newServerState
Notifications[.didChangeServerCurrentURI].post(object: newServerState) Notifications[.didChangeServerCurrentURI].post(object: newServerState)
} }
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -20,7 +20,7 @@ class ServerListViewModel: ObservableObject {
// This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing. // This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing.
// Feature request issue: https://github.com/rundfunk47/stinsen/issues/33 // Feature request issue: https://github.com/rundfunk47/stinsen/issues/33
// Go to each MainCoordinator and implement the rebuild of the root when receiving the notification // Go to each MainCoordinator and implement the rebuild of the root when receiving the notification
Notifications[.didPurge].subscribe(self, selector: #selector(didPurge)) Notifications[.didPurge].subscribe(self, selector: #selector(didPurge))
} }
func fetchServers() { func fetchServers() {

View File

@ -21,7 +21,7 @@ class UserListViewModel: ViewModel {
super.init() super.init()
Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeCurrentLoginURI(_:))) Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeCurrentLoginURI(_:)))
} }
@objc @objc

View File

@ -585,7 +585,7 @@ extension VideoPlayerViewModel {
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
LogManager.shared.log.debug("Stop report sent for item: \(self.item.id ?? "No ID")") LogManager.shared.log.debug("Stop report sent for item: \(self.item.id ?? "No ID")")
Notifications[.didSendStopReport].post(object: self.item.id) Notifications[.didSendStopReport].post(object: self.item.id)
} }
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -11,51 +11,52 @@ import UIKit
struct BlurHashView: UIViewRepresentable { struct BlurHashView: UIViewRepresentable {
let blurHash: String let blurHash: String
func makeUIView(context: Context) -> UIBlurHashView { func makeUIView(context: Context) -> UIBlurHashView {
return UIBlurHashView(blurHash) UIBlurHashView(blurHash)
} }
func updateUIView(_ uiView: UIBlurHashView, context: Context) {} func updateUIView(_ uiView: UIBlurHashView, context: Context) {}
} }
class UIBlurHashView: UIView { class UIBlurHashView: UIView {
private let imageView: UIImageView private let imageView: UIImageView
init(_ blurHash: String) { init(_ blurHash: String) {
let imageView = UIImageView() let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
self.imageView = imageView self.imageView = imageView
super.init(frame: .zero) super.init(frame: .zero)
computeBlurHashImageAsync(blurHash: blurHash) { blurImage in computeBlurHashImageAsync(blurHash: blurHash) { blurImage in
DispatchQueue.main.async { DispatchQueue.main.async {
self.imageView.image = blurImage self.imageView.image = blurImage
self.imageView.setNeedsDisplay() self.imageView.setNeedsDisplay()
} }
} }
addSubview(imageView) addSubview(imageView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor), imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor), imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.leftAnchor.constraint(equalTo: leftAnchor), imageView.leftAnchor.constraint(equalTo: leftAnchor),
imageView.rightAnchor.constraint(equalTo: rightAnchor), imageView.rightAnchor.constraint(equalTo: rightAnchor),
]) ])
} }
required init?(coder: NSCoder) { @available(*, unavailable)
fatalError("init(coder:) has not been implemented") required init?(coder: NSCoder) {
} fatalError("init(coder:) has not been implemented")
}
private func computeBlurHashImageAsync(blurHash: String, _ completion: @escaping (UIImage?) -> Void) { private func computeBlurHashImageAsync(blurHash: String, _ completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
let image = UIImage(blurHash: blurHash, size: .Circle(radius: 12)) let image = UIImage(blurHash: blurHash, size: .Circle(radius: 12))
completion(image) completion(image)
} }
} }
} }

View File

@ -14,92 +14,92 @@ import UIKit
// TODO: Fix 100+ inits // TODO: Fix 100+ inits
struct ImageViewSource { struct ImageViewSource {
let url: URL? let url: URL?
let blurHash: String? let blurHash: String?
init(url: URL? = nil, blurHash: String? = nil) { init(url: URL? = nil, blurHash: String? = nil) {
self.url = url self.url = url
self.blurHash = blurHash self.blurHash = blurHash
} }
} }
struct DefaultFailureView: View { struct DefaultFailureView: View {
var body: some View { var body: some View {
Color.secondary Color.secondary
} }
} }
struct ImageView<FailureView: View>: View { struct ImageView<FailureView: View>: View {
@State @State
private var sources: [ImageViewSource] private var sources: [ImageViewSource]
private var currentURL: URL? { sources.first?.url } private var currentURL: URL? { sources.first?.url }
private var currentBlurHash: String? { sources.first?.blurHash } private var currentBlurHash: String? { sources.first?.blurHash }
private var failureView: FailureView private var failureView: FailureView
init(_ source: URL?, blurHash: String? = nil, @ViewBuilder failureView: () -> FailureView) { init(_ source: URL?, blurHash: String? = nil, @ViewBuilder failureView: () -> FailureView) {
let imageViewSource = ImageViewSource(url: source, blurHash: blurHash) let imageViewSource = ImageViewSource(url: source, blurHash: blurHash)
_sources = State(initialValue: [imageViewSource]) _sources = State(initialValue: [imageViewSource])
self.failureView = failureView() self.failureView = failureView()
} }
init(_ source: ImageViewSource, @ViewBuilder failureView: () -> FailureView) { init(_ source: ImageViewSource, @ViewBuilder failureView: () -> FailureView) {
_sources = State(initialValue: [source]) _sources = State(initialValue: [source])
self.failureView = failureView() self.failureView = failureView()
} }
init(_ sources: [ImageViewSource], @ViewBuilder failureView: () -> FailureView) { init(_ sources: [ImageViewSource], @ViewBuilder failureView: () -> FailureView) {
_sources = State(initialValue: sources) _sources = State(initialValue: sources)
self.failureView = failureView() self.failureView = failureView()
} }
@ViewBuilder @ViewBuilder
private var placeholderView: some View { private var placeholderView: some View {
if let currentBlurHash = currentBlurHash { if let currentBlurHash = currentBlurHash {
BlurHashView(blurHash: currentBlurHash) BlurHashView(blurHash: currentBlurHash)
.id(currentBlurHash) .id(currentBlurHash)
} else { } else {
Color.secondary Color.secondary
} }
} }
var body: some View { var body: some View {
if let currentURL = currentURL { if let currentURL = currentURL {
LazyImage(source: currentURL) { state in LazyImage(source: currentURL) { state in
if let image = state.image { if let image = state.image {
image image
} else if state.error != nil { } else if state.error != nil {
placeholderView.onAppear { sources.removeFirst() } placeholderView.onAppear { sources.removeFirst() }
} else { } else {
placeholderView placeholderView
} }
} }
.pipeline(ImagePipeline(configuration: .withDataCache)) .pipeline(ImagePipeline(configuration: .withDataCache))
.id(currentURL) .id(currentURL)
} else { } else {
failureView failureView
} }
} }
} }
extension ImageView where FailureView == DefaultFailureView { extension ImageView where FailureView == DefaultFailureView {
init(_ source: URL?, blurHash: String? = nil) { init(_ source: URL?, blurHash: String? = nil) {
let imageViewSource = ImageViewSource(url: source, blurHash: blurHash) let imageViewSource = ImageViewSource(url: source, blurHash: blurHash)
self.init(imageViewSource, failureView: { DefaultFailureView() }) self.init(imageViewSource, failureView: { DefaultFailureView() })
} }
init(_ source: ImageViewSource) { init(_ source: ImageViewSource) {
self.init(source, failureView: { DefaultFailureView() }) self.init(source, failureView: { DefaultFailureView() })
} }
init(_ sources: [ImageViewSource]) { init(_ sources: [ImageViewSource]) {
self.init(sources, failureView: { DefaultFailureView() }) self.init(sources, failureView: { DefaultFailureView() })
} }
init(sources: [URL]) { init(sources: [URL]) {
let imageViewSources = sources.compactMap { ImageViewSource(url: $0, blurHash: nil) } let imageViewSources = sources.compactMap { ImageViewSource(url: $0, blurHash: nil) }
self.init(imageViewSources, failureView: { DefaultFailureView() }) self.init(imageViewSources, failureView: { DefaultFailureView() })
} }
} }

View File

@ -10,21 +10,21 @@ import SwiftUI
struct InitialFailureView: View { struct InitialFailureView: View {
let initials: String let initials: String
init(_ initials: String) { init(_ initials: String) {
self.initials = initials self.initials = initials
} }
var body: some View { var body: some View {
ZStack { ZStack {
Rectangle() Rectangle()
.foregroundColor(Color(UIColor.darkGray)) .foregroundColor(Color(UIColor.darkGray))
Text(initials) Text(initials)
.font(.largeTitle) .font(.largeTitle)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.accessibilityHidden(true) .accessibilityHidden(true)
} }
} }
} }

View File

@ -38,6 +38,31 @@ struct LibraryListView: View {
self.mainCoordinator.root(\.liveTV) self.mainCoordinator.root(\.liveTV)
} }
label: { label: {
ZStack {
HStack {
Spacer()
VStack {
Text(library.name ?? "")
.foregroundColor(.white)
.font(.title2)
.fontWeight(.semibold)
}
Spacer()
}.padding(32)
}
.frame(minWidth: 100, maxWidth: .infinity)
.frame(height: 100)
}
.cornerRadius(10)
.shadow(radius: 5)
.padding(.bottom, 5)
}
} else {
Button {
self.libraryListRouter.route(to: \.library,
(viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? ""))
}
label: {
ZStack { ZStack {
HStack { HStack {
Spacer() Spacer()
@ -56,31 +81,6 @@ struct LibraryListView: View {
.cornerRadius(10) .cornerRadius(10)
.shadow(radius: 5) .shadow(radius: 5)
.padding(.bottom, 5) .padding(.bottom, 5)
}
} else {
Button {
self.libraryListRouter.route(to: \.library,
(viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? ""))
}
label: {
ZStack {
HStack {
Spacer()
VStack {
Text(library.name ?? "")
.foregroundColor(.white)
.font(.title2)
.fontWeight(.semibold)
}
Spacer()
}.padding(32)
}
.frame(minWidth: 100, maxWidth: .infinity)
.frame(height: 100)
}
.cornerRadius(10)
.shadow(radius: 5)
.padding(.bottom, 5)
} }
} }
} else { } else {

View File

@ -81,7 +81,7 @@ 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)) Notifications[.processDeepLink].post(object: DeepLink.item(item))
} }
return true return true

View File

@ -45,12 +45,12 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
VStack(alignment: horizontalAlignment) { VStack(alignment: horizontalAlignment) {
ImageView(item.imageURLConstructor(maxWidth: Int(maxWidth)), ImageView(item.imageURLConstructor(maxWidth: Int(maxWidth)),
blurHash: item.blurHash, blurHash: item.blurHash,
failureView: { failureView: {
InitialFailureView(item.failureInitials) InitialFailureView(item.failureInitials)
}) })
.portraitPoster(width: maxWidth) .portraitPoster(width: maxWidth)
.shadow(radius: 4, y: 2) .shadow(radius: 4, y: 2)
.accessibilityIgnoresInvertColors() .accessibilityIgnoresInvertColors()
if item.showTitle { if item.showTitle {
Text(item.title) Text(item.title)

View File

@ -37,12 +37,12 @@ struct PortraitItemButton<ItemType: PortraitImageStackable>: View {
VStack(alignment: horizontalAlignment) { VStack(alignment: horizontalAlignment) {
ImageView(item.imageURLConstructor(maxWidth: Int(maxWidth)), ImageView(item.imageURLConstructor(maxWidth: Int(maxWidth)),
blurHash: item.blurHash, blurHash: item.blurHash,
failureView: { failureView: {
InitialFailureView(item.failureInitials) InitialFailureView(item.failureInitials)
}) })
.portraitPoster(width: maxWidth) .portraitPoster(width: maxWidth)
.shadow(radius: 4, y: 2) .shadow(radius: 4, y: 2)
.accessibilityIgnoresInvertColors() .accessibilityIgnoresInvertColors()
if item.showTitle { if item.showTitle {
Text(item.title) Text(item.title)

View File

@ -755,7 +755,7 @@ extension VLCPlayerViewController {
extension VLCPlayerViewController: VLCMediaPlayerDelegate { extension VLCPlayerViewController: VLCMediaPlayerDelegate {
// MARK: mediaPlayerStateChanged // MARK: mediaPlayerStateChanged
func mediaPlayerStateChanged(_ aNotification: Notification) { func mediaPlayerStateChanged(_ aNotification: Notification) {
// Don't show buffering if paused, usually here while scrubbing // Don't show buffering if paused, usually here while scrubbing
if vlcMediaPlayer.state == .buffering, viewModel.playerState == .paused { if vlcMediaPlayer.state == .buffering, viewModel.playerState == .paused {
return return
@ -774,7 +774,7 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
// MARK: mediaPlayerTimeChanged // MARK: mediaPlayerTimeChanged
func mediaPlayerTimeChanged(_ aNotification: Notification) { func mediaPlayerTimeChanged(_ aNotification: Notification) {
if !viewModel.sliderIsScrubbing { if !viewModel.sliderIsScrubbing {
viewModel.sliderPercentage = Double(vlcMediaPlayer.position) viewModel.sliderPercentage = Double(vlcMediaPlayer.position)
} }