Merge pull request #393 from LePips/improve-image-views

This commit is contained in:
Ethan Pippin 2022-03-19 11:17:25 -06:00 committed by GitHub
commit 1ab99a6f2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 322 additions and 154 deletions

View File

@ -45,12 +45,10 @@ final class MainCoordinator: NavigationCoordinatable {
barAppearance.tintColor = UIColor(Color.jellyfinPurple)
// Notification setup for state
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil)
nc.addObserver(self, selector: #selector(didChangeServerCurrentURI),
name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil)
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
@ -60,13 +58,13 @@ final class MainCoordinator: NavigationCoordinatable {
}
@objc
func didLogIn() {
func didSignIn() {
LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.")
root(\.mainTab)
}
@objc
func didLogOut() {
func didSignOut() {
LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.")
root(\.serverList)
}

View File

@ -40,19 +40,18 @@ final class MainCoordinator: NavigationCoordinatable {
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
// Notification setup for state
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
}
@objc
func didLogIn() {
func didSignIn() {
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
root(\.mainTab)
}
@objc
func didLogOut() {
func didSignOut() {
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
root(\.serverList)
}

View File

@ -240,7 +240,7 @@ final class SessionManager {
Defaults[.lastServerUserID] = user.id
currentLogin = (server: currentServer.state, user: currentUser.state)
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
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)
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
Notifications[.didSignIn].post()
}
// MARK: logout
@ -265,7 +265,7 @@ final class SessionManager {
JellyfinAPI.basePath = ""
setAuthHeader(with: "")
Defaults[.lastServerUserID] = nil
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
Notifications[.didSignOut].post()
}
// MARK: purge
@ -278,7 +278,7 @@ final class SessionManager {
delete(server: server)
}
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
Notifications[.didPurge].post()
}
// MARK: delete user

View File

@ -8,20 +8,63 @@
import Foundation
enum SwiftfinNotificationCenter {
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)
}
}
enum Notifications {
static let main: NotificationCenter = {
NotificationCenter()
}()
enum Keys {
static let didSignIn = Notification.Name("didSignIn")
static let didSignOut = Notification.Name("didSignOut")
static let processDeepLink = Notification.Name("processDeepLink")
static let didPurge = Notification.Name("didPurge")
static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI")
final class Key {
public typealias NotificationKey = Notifications.Key
// Send with an item id to check if current item for item views
static let didSendStopReport = Notification.Name("didSendStopReport")
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")
}

View File

@ -34,9 +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
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
}
@objc

View File

@ -55,10 +55,7 @@ class ItemViewModel: ViewModel {
getSimilarItems()
SwiftfinNotificationCenter.main.addObserver(self,
selector: #selector(receivedStopReport(_:)),
name: SwiftfinNotificationCenter.Keys.didSendStopReport,
object: nil)
Notifications[.didSendStopReport].subscribe(self, selector: #selector(receivedStopReport(_:)))
refreshItemVideoPlayerViewModel(for: item)
}
@ -72,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
SwiftfinNotificationCenter.main.removeObserver(self)
Notifications.unsubscribe(self)
}
}

View File

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

View File

@ -20,8 +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
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
Notifications[.didPurge].subscribe(self, selector: #selector(didPurge))
}
func fetchServers() {

View File

@ -21,9 +21,7 @@ class UserListViewModel: ViewModel {
super.init()
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didChangeCurrentLoginURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI,
object: nil)
Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeCurrentLoginURI(_:)))
}
@objc

View File

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

View File

@ -0,0 +1,63 @@
//
// 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) 2022 Jellyfin & Jellyfin Contributors
//
import SwiftUI
import UIKit
struct BlurHashView: UIViewRepresentable {
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) { [weak self] blurImage in
guard let self = self else { return }
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

@ -6,58 +6,68 @@
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Nuke
import NukeUI
import SwiftUI
import UIKit
// TODO: update multiple sources so that multiple blurhashes can be taken, clean up
// TODO: Fix 100+ inits
struct ImageView: View {
struct ImageViewSource {
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
}
}
struct ImageView<FailureView: View>: View {
@State
private var sources: [URL]
private var currentURL: URL? { sources.first }
private var sources: [ImageViewSource]
private var currentURL: URL? { sources.first?.url }
private var currentBlurHash: String? { sources.first?.blurHash }
private var failureView: FailureView
private let blurhash: String
private let failureInitials: String
init(src: URL, bh: String = "001fC^", failureInitials: String = "") {
self.sources = [src]
self.blurhash = bh
self.failureInitials = failureInitials
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: [URL], bh: String = "001fC^", failureInitials: String = "") {
assert(!sources.isEmpty, "Must supply at least one source")
self.sources = sources
self.blurhash = bh
self.failureInitials = failureInitials
init(_ source: ImageViewSource, @ViewBuilder failureView: () -> FailureView) {
_sources = State(initialValue: [source])
self.failureView = failureView()
}
init(_ sources: [ImageViewSource], @ViewBuilder failureView: () -> FailureView) {
_sources = State(initialValue: sources)
self.failureView = failureView()
}
// TODO: fix placeholder hash view
@ViewBuilder
private var placeholderView: some View {
Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 12, height: 12)) ??
UIImage(blurHash: "001fC^", size: CGSize(width: 12, height: 12))!)
.resizable()
}
@ViewBuilder
private func failureImage() -> some View {
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.darkGray))
Text(failureInitials)
.font(.largeTitle)
.foregroundColor(.secondary)
.accessibilityHidden(true)
if let currentBlurHash = currentBlurHash {
BlurHashView(blurHash: currentBlurHash)
.id(currentBlurHash)
} else {
Color.secondary
}
}
var body: some View {
if let u = currentURL {
LazyImage(source: u) { state in
if let currentURL = currentURL {
LazyImage(source: currentURL) { state in
if let image = state.image {
image
} else if state.error != nil {
@ -67,9 +77,29 @@ struct ImageView: View {
}
}
.pipeline(ImagePipeline(configuration: .withDataCache))
.id(u)
.id(currentURL)
} else {
failureImage()
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(_ 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

@ -0,0 +1,30 @@
//
// 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) 2022 Jellyfin & Jellyfin Contributors
//
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)
}
}
}

View File

@ -29,7 +29,7 @@ struct LiveTVChannelItemElement: View {
.font(.footnote)
.frame(alignment: .trailing)
}.frame(alignment: .top)
ImageView(src: channel.getPrimaryImage(maxWidth: 125))
ImageView(channel.getPrimaryImage(maxWidth: 125))
.frame(width: 125, alignment: .center)
.offset(x: 0, y: -32)
Text(channel.name ?? "?")

View File

@ -21,8 +21,8 @@ struct EpisodeRowCard: View {
Button {
itemRouter.route(to: \.item, episode)
} label: {
ImageView(src: episode.getBackdropImage(maxWidth: 550),
bh: episode.getBackdropImageBlurHash())
ImageView(episode.getBackdropImage(maxWidth: 550),
blurHash: episode.getBackdropImageBlurHash())
.mask(Rectangle().frame(width: 550, height: 308))
.frame(width: 550, height: 308)
}

View File

@ -30,11 +30,10 @@ struct CinematicNextUpCardView: View {
])
.frame(width: 350, height: 210)
} else {
ImageView(sources: [
item.getThumbImage(maxWidth: 350),
item.getBackdropImage(maxWidth: 350),
],
bh: item.getBackdropImageBlurHash())
ImageView([
.init(url: item.getThumbImage(maxWidth: 350)),
.init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()),
])
.frame(width: 350, height: 210)
}

View File

@ -31,11 +31,10 @@ struct CinematicResumeCardView: View {
])
.frame(width: 350, height: 210)
} else {
ImageView(sources: [
item.getThumbImage(maxWidth: 350),
item.getBackdropImage(maxWidth: 350),
],
bh: item.getBackdropImageBlurHash())
ImageView([
.init(url: item.getThumbImage(maxWidth: 350)),
.init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()),
])
.frame(width: 350, height: 210)
}

View File

@ -46,9 +46,9 @@ struct LandscapeItemElement: View {
var body: some View {
VStack {
ImageView(src: item.type == "Episode" && !(inSeasonView ?? false) ? item.getSeriesBackdropImage(maxWidth: 445) : item
ImageView(item.type == "Episode" && !(inSeasonView ?? false) ? item.getSeriesBackdropImage(maxWidth: 445) : item
.getBackdropImage(maxWidth: 445),
bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash())
blurHash: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash())
.frame(width: 445, height: 250)
.cornerRadius(10)
.ignoresSafeArea()

View File

@ -21,8 +21,8 @@ struct PortraitItemElement: View {
var body: some View {
VStack {
ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200),
bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash())
ImageView(item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200),
blurHash: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash())
.frame(width: 200, height: 300)
.cornerRadius(10)
.shadow(radius: focused ? 10.0 : 0)

View File

@ -45,7 +45,7 @@ struct PortraitItemsRowView: View {
Button {
selectedAction(item)
} label: {
ImageView(src: item.portraitHeaderViewURL(maxWidth: 257))
ImageView(item.portraitHeaderViewURL(maxWidth: 257))
.frame(width: 257, height: 380)
}
.frame(height: 380)

View File

@ -20,7 +20,7 @@ struct PublicUserButton: View {
var body: some View {
VStack {
if publicUser.primaryImageTag != nil {
ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!)
ImageView(URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!)
.frame(width: 250, height: 250)
.cornerRadius(125.0)
} else {

View File

@ -23,10 +23,10 @@ struct ContinueWatchingCard: View {
ZStack(alignment: .bottom) {
if item.itemType == .episode {
ImageView(src: item.getSeriesBackdropImage(maxWidth: 500))
ImageView(item.getSeriesBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 500))
ImageView(item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
}

View File

@ -24,8 +24,8 @@ struct CinematicCollectionItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920),
blurHash: viewModel.item.getBackdropImageBlurHash())
.ignoresSafeArea()
ScrollView {

View File

@ -32,8 +32,8 @@ struct CinematicEpisodeItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920),
blurHash: viewModel.item.getBackdropImageBlurHash())
.frame(height: UIScreen.main.bounds.height - 10)
.ignoresSafeArea()

View File

@ -17,7 +17,7 @@ struct CinematicItemAboutView: View {
var body: some View {
HStack(alignment: .top, spacing: 10) {
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 257))
ImageView(viewModel.item.portraitHeaderViewURL(maxWidth: 257))
.portraitPoster(width: 257)
ZStack(alignment: .topLeading) {

View File

@ -24,8 +24,8 @@ struct CinematicMovieItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920),
blurHash: viewModel.item.getBackdropImageBlurHash())
.ignoresSafeArea()
ScrollView {

View File

@ -23,7 +23,7 @@ struct CinematicSeasonItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash())
.ignoresSafeArea()
ScrollView {

View File

@ -23,7 +23,7 @@ struct CinematicSeriesItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash())
.ignoresSafeArea()
ScrollView {

View File

@ -45,7 +45,7 @@ struct EpisodeItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash())
.opacity(0.4)
.ignoresSafeArea()
LazyVStack(alignment: .leading) {

View File

@ -50,7 +50,7 @@ struct MovieItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash())
.opacity(0.4)
.ignoresSafeArea()
ScrollView {

View File

@ -25,7 +25,7 @@ struct SeasonItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 1920), bh: viewModel.item.getSeriesBackdropImageBlurHash())
ImageView(viewModel.item.getSeriesBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getSeriesBackdropImageBlurHash())
.opacity(0.4)
.ignoresSafeArea()
ScrollView {

View File

@ -53,7 +53,7 @@ struct SeriesItemView: View {
var body: some View {
ZStack {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash())
.opacity(0.4)
.ignoresSafeArea()
ScrollView {

View File

@ -34,7 +34,7 @@ struct LatestMediaView: View {
Button {
homeRouter.route(to: \.modalItem, item)
} label: {
ImageView(src: item.portraitHeaderViewURL(maxWidth: 257))
ImageView(item.portraitHeaderViewURL(maxWidth: 257))
.frame(width: 257, height: 380)
}
.frame(height: 380)

View File

@ -21,10 +21,10 @@ struct NextUpCard: View {
homeRouter.route(to: \.modalItem, item)
} label: {
if item.itemType == .episode {
ImageView(src: item.getSeriesBackdropImage(maxWidth: 500))
ImageView(item.getSeriesBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 500))
ImageView(item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
}
}

View File

@ -700,7 +700,7 @@ extension LiveTVPlayerViewController: 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 {
@ -720,7 +720,7 @@ extension LiveTVPlayerViewController: VLCMediaPlayerDelegate {
// MARK: mediaPlayerTimeChanged
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
func mediaPlayerTimeChanged(_ aNotification: Notification) {
if !viewModel.sliderIsScrubbing {
viewModel.sliderPercentage = Double(vlcMediaPlayer.position)

View File

@ -329,7 +329,7 @@ struct SmallMediaStreamSelectionView: View {
Button {
viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex])
} label: {
ImageView(src: chapterImages[chapterIndex])
ImageView(chapterImages[chapterIndex])
.cornerRadius(10)
.frame(width: 350, height: 210)
}

View File

@ -700,7 +700,7 @@ 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 {
@ -720,7 +720,7 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
// MARK: mediaPlayerTimeChanged
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
func mediaPlayerTimeChanged(_ aNotification: Notification) {
if !viewModel.sliderIsScrubbing {
viewModel.sliderPercentage = Double(vlcMediaPlayer.position)

View File

@ -292,6 +292,10 @@
E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */; };
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */; };
E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */; };
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */; };
E1047E2127E584AF00CB0D4A /* BlurHashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */; };
E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; };
E1047E2427E5880000CB0D4A /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; };
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB952788104100354E07 /* CinematicCollectionItemView.swift */; };
@ -760,6 +764,8 @@
E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCinematicView.swift; sourceTree = "<group>"; };
E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeCardView.swift; sourceTree = "<group>"; };
E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicNextUpCardView.swift; sourceTree = "<group>"; };
E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashView.swift; sourceTree = "<group>"; };
E1047E2227E5880000CB0D4A /* InitialFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialFailureView.swift; sourceTree = "<group>"; };
E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; };
E107BB952788104100354E07 /* CinematicCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicCollectionItemView.swift; sourceTree = "<group>"; };
E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemSize.swift; sourceTree = "<group>"; };
@ -1748,7 +1754,9 @@
E1AD105326D96F5A003E4A08 /* Views */ = {
isa = PBXGroup;
children = (
E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */,
531AC8BE26750DE20091C7EB /* ImageView.swift */,
E1047E2227E5880000CB0D4A /* InitialFailureView.swift */,
621338B22660A07800A81A2A /* LazyView.swift */,
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */,
@ -2243,6 +2251,7 @@
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */,
53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */,
E1047E2127E584AF00CB0D4A /* BlurHashView.swift in Sources */,
531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */,
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
@ -2258,6 +2267,7 @@
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */,
E1047E2427E5880000CB0D4A /* InitialFailureView.swift in Sources */,
E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */,
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */,
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
@ -2390,9 +2400,11 @@
E1EBCB42278BD174009FE6E9 /* TruncatedTextView.swift in Sources */,
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */,
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */,
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */,
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */,
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */,

View File

@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/kaishin/Gifu",
"state" : {
"revision" : "51f2eab32903e336f590c013267cfa4d7f8b06c4",
"version" : "3.3.1"
"revision" : "0ffe24744cc3d82ab9edece53670d0352c6d5507",
"version" : "3.3.0"
}
},
{
@ -66,7 +66,7 @@
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke",
"location" : "https://github.com/kean/Nuke.git",
"state" : {
"revision" : "78fa963b8491fc520791d8c2a509f1b8593d8aae",
"version" : "10.7.1"
@ -140,8 +140,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect",
"state" : {
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
"version" : "0.1.4"
"revision" : "2e09be8af614401bc9f87d40093ec19ce56ccaf2",
"version" : "0.1.3"
}
},
{

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 }
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.processDeepLink, object: DeepLink.item(item))
Notifications[.processDeepLink].post(object: DeepLink.item(item))
}
return true

View File

@ -23,8 +23,8 @@ struct EpisodeRowCard: View {
HStack(alignment: .top) {
VStack(alignment: .leading) {
ImageView(src: episode.getBackdropImage(maxWidth: 200),
bh: episode.getBackdropImageBlurHash())
ImageView(episode.getBackdropImage(maxWidth: 200),
blurHash: episode.getBackdropImageBlurHash())
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
.frame(width: 200, height: 112)
.overlay {

View File

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

View File

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

View File

@ -24,8 +24,8 @@ struct ItemLandscapeMainView: View {
// MARK: Sidebar Image
VStack {
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130),
bh: viewModel.item.getPrimaryImageBlurHash())
ImageView(viewModel.item.portraitHeaderViewURL(maxWidth: 130),
blurHash: viewModel.item.getPrimaryImageBlurHash())
.frame(width: 130, height: 195)
.cornerRadius(10)
.accessibilityIgnoresInvertColors()
@ -95,8 +95,8 @@ struct ItemLandscapeMainView: View {
ZStack {
// MARK: Backdrop
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200),
bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: 200),
blurHash: viewModel.item.getBackdropImageBlurHash())
.opacity(0.3)
.edgesIgnoringSafeArea(.all)
.blur(radius: 8)

View File

@ -24,8 +24,8 @@ struct PortraitHeaderOverlayView: View {
// MARK: Portrait Image
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130),
bh: viewModel.item.getPrimaryImageBlurHash())
ImageView(viewModel.item.portraitHeaderViewURL(maxWidth: 130),
blurHash: viewModel.item.getPrimaryImageBlurHash())
.portraitPoster(width: 130)
.accessibilityIgnoresInvertColors()

View File

@ -19,8 +19,8 @@ struct ItemPortraitMainView: View {
// MARK: portraitHeaderView
var portraitHeaderView: some View {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)),
bh: viewModel.item.getBackdropImageBlurHash())
ImageView(viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)),
blurHash: viewModel.item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
.accessibilityIgnoresInvertColors()

View File

@ -54,7 +54,7 @@ struct LibraryListView: View {
title: library.name ?? ""))
} label: {
ZStack {
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
ImageView(library.getPrimaryImage(maxWidth: 500), blurHash: library.getPrimaryImageBlurHash())
.opacity(0.4)
.accessibilityIgnoresInvertColors()
HStack {

View File

@ -48,7 +48,7 @@ struct VLCPlayerChapterOverlayView: View {
Button {
viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex])
} label: {
ImageView(src: chapterImages[chapterIndex])
ImageView(chapterImages[chapterIndex])
.cornerRadius(10)
.frame(width: 150, height: 100)
.overlay {

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