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)
// Notification setup for state
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
Notifications[.processDeepLink].subscribe(self, selector: #selector(processDeepLink(_:)))
Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeServerCurrentURI(_:)))
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
Notifications[.processDeepLink].subscribe(self, selector: #selector(processDeepLink(_:)))
Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeServerCurrentURI(_:)))
Defaults.publisher(.appAppearance)
.sink { _ in

View File

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

View File

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

View File

@ -34,8 +34,8 @@ final class HomeViewModel: ViewModel {
// Nov. 6, 2021
// 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
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
}
@objc

View File

@ -54,8 +54,8 @@ class ItemViewModel: ViewModel {
super.init()
getSimilarItems()
Notifications[.didSendStopReport].subscribe(self, selector: #selector(receivedStopReport(_:)))
Notifications[.didSendStopReport].subscribe(self, selector: #selector(receivedStopReport(_:)))
refreshItemVideoPlayerViewModel(for: item)
}
@ -69,7 +69,7 @@ class ItemViewModel: ViewModel {
} else {
// Remove if necessary. Note that this cannot be in deinit as
// 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
self.server = newServerState
Notifications[.didChangeServerCurrentURI].post(object: newServerState)
Notifications[.didChangeServerCurrentURI].post(object: newServerState)
}
.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.
// 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
Notifications[.didPurge].subscribe(self, selector: #selector(didPurge))
Notifications[.didPurge].subscribe(self, selector: #selector(didPurge))
}
func fetchServers() {

View File

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

View File

@ -585,7 +585,7 @@ extension VideoPlayerViewModel {
self.handleAPIRequestError(completion: completion)
} receiveValue: { _ in
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)
}

View File

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

View File

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

View File

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

View File

@ -38,6 +38,31 @@ struct LibraryListView: View {
self.mainCoordinator.root(\.liveTV)
}
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 {
HStack {
Spacer()
@ -56,31 +81,6 @@ struct LibraryListView: View {
.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 {
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 {

View File

@ -81,7 +81,7 @@ extension AppURLHandler {
// It would be nice if the ItemViewModel could be initialized to id later.
getItem(userID: userID, itemID: itemID) { item in
guard let item = item else { return }
Notifications[.processDeepLink].post(object: DeepLink.item(item))
Notifications[.processDeepLink].post(object: DeepLink.item(item))
}
return true

View File

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

View File

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

View File

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