mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
Media refactoring
This commit is contained in:
parent
264881f9b0
commit
ff2f813280
18 changed files with 168 additions and 114 deletions
|
@ -1,10 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import ViewModels
|
|
||||||
|
|
||||||
extension AppPreferences {
|
|
||||||
var shouldReduceMotion: Bool {
|
|
||||||
UIAccessibility.isReduceMotionEnabled && useSystemReduceMotionForMedia
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,7 +27,6 @@
|
||||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */; };
|
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */; };
|
||||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */; };
|
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */; };
|
||||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||||
D0A3C2F725390A9700739F88 /* AppPreferences+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */; };
|
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
||||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreCell.swift */; };
|
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreCell.swift */; };
|
||||||
|
@ -131,7 +130,6 @@
|
||||||
D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomTransitionController.swift; sourceTree = "<group>"; };
|
D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomTransitionController.swift; sourceTree = "<group>"; };
|
||||||
D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomAnimatableView.swift; sourceTree = "<group>"; };
|
D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomAnimatableView.swift; sourceTree = "<group>"; };
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPreferences+Extensions.swift"; sourceTree = "<group>"; };
|
|
||||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||||
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = "<group>"; };
|
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -384,7 +382,6 @@
|
||||||
D0C7D46824F76169001EBDBB /* Extensions */ = {
|
D0C7D46824F76169001EBDBB /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */,
|
|
||||||
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */,
|
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */,
|
||||||
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
|
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
|
||||||
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
|
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
|
||||||
|
@ -614,7 +611,6 @@
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
D0A3C2F725390A9700739F88 /* AppPreferences+Extensions.swift in Sources */,
|
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
||||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||||
|
|
|
@ -13,6 +13,7 @@ public struct AppEnvironment {
|
||||||
let keychain: Keychain.Type
|
let keychain: Keychain.Type
|
||||||
let userDefaults: UserDefaults
|
let userDefaults: UserDefaults
|
||||||
let userNotificationClient: UserNotificationClient
|
let userNotificationClient: UserNotificationClient
|
||||||
|
let reduceMotion: () -> Bool
|
||||||
let uuid: () -> UUID
|
let uuid: () -> UUID
|
||||||
let inMemoryContent: Bool
|
let inMemoryContent: Bool
|
||||||
let fixtureDatabase: IdentityDatabase?
|
let fixtureDatabase: IdentityDatabase?
|
||||||
|
@ -22,6 +23,7 @@ public struct AppEnvironment {
|
||||||
keychain: Keychain.Type,
|
keychain: Keychain.Type,
|
||||||
userDefaults: UserDefaults,
|
userDefaults: UserDefaults,
|
||||||
userNotificationClient: UserNotificationClient,
|
userNotificationClient: UserNotificationClient,
|
||||||
|
reduceMotion: @escaping () -> Bool,
|
||||||
uuid: @escaping () -> UUID,
|
uuid: @escaping () -> UUID,
|
||||||
inMemoryContent: Bool,
|
inMemoryContent: Bool,
|
||||||
fixtureDatabase: IdentityDatabase?) {
|
fixtureDatabase: IdentityDatabase?) {
|
||||||
|
@ -30,6 +32,7 @@ public struct AppEnvironment {
|
||||||
self.keychain = keychain
|
self.keychain = keychain
|
||||||
self.userDefaults = userDefaults
|
self.userDefaults = userDefaults
|
||||||
self.userNotificationClient = userNotificationClient
|
self.userNotificationClient = userNotificationClient
|
||||||
|
self.reduceMotion = reduceMotion
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.inMemoryContent = inMemoryContent
|
self.inMemoryContent = inMemoryContent
|
||||||
self.fixtureDatabase = fixtureDatabase
|
self.fixtureDatabase = fixtureDatabase
|
||||||
|
@ -37,13 +40,14 @@ public struct AppEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension AppEnvironment {
|
public extension AppEnvironment {
|
||||||
static func live(userNotificationCenter: UNUserNotificationCenter) -> Self {
|
static func live(userNotificationCenter: UNUserNotificationCenter, reduceMotion: @escaping () -> Bool) -> Self {
|
||||||
Self(
|
Self(
|
||||||
session: URLSession.shared,
|
session: URLSession.shared,
|
||||||
webAuthSessionType: LiveWebAuthSession.self,
|
webAuthSessionType: LiveWebAuthSession.self,
|
||||||
keychain: LiveKeychain.self,
|
keychain: LiveKeychain.self,
|
||||||
userDefaults: .standard,
|
userDefaults: .standard,
|
||||||
userNotificationClient: .live(userNotificationCenter),
|
userNotificationClient: .live(userNotificationCenter),
|
||||||
|
reduceMotion: reduceMotion,
|
||||||
uuid: UUID.init,
|
uuid: UUID.init,
|
||||||
inMemoryContent: false,
|
inMemoryContent: false,
|
||||||
fixtureDatabase: nil)
|
fixtureDatabase: nil)
|
||||||
|
|
|
@ -5,9 +5,11 @@ import Foundation
|
||||||
|
|
||||||
public struct AppPreferences {
|
public struct AppPreferences {
|
||||||
private let userDefaults: UserDefaults
|
private let userDefaults: UserDefaults
|
||||||
|
private let systemReduceMotion: () -> Bool
|
||||||
|
|
||||||
public init(environment: AppEnvironment) {
|
public init(environment: AppEnvironment) {
|
||||||
self.userDefaults = environment.userDefaults
|
self.userDefaults = environment.userDefaults
|
||||||
|
self.systemReduceMotion = environment.reduceMotion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +75,10 @@ public extension AppPreferences {
|
||||||
}
|
}
|
||||||
set { self[.autoplayVideos] = newValue.rawValue }
|
set { self[.autoplayVideos] = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldReduceMotion: Bool {
|
||||||
|
systemReduceMotion() && useSystemReduceMotionForMedia
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppPreferences {
|
extension AppPreferences {
|
||||||
|
|
|
@ -23,6 +23,7 @@ public extension AppEnvironment {
|
||||||
keychain: keychain,
|
keychain: keychain,
|
||||||
userDefaults: userDefaults,
|
userDefaults: userDefaults,
|
||||||
userNotificationClient: userNotificationClient,
|
userNotificationClient: userNotificationClient,
|
||||||
|
reduceMotion: { false },
|
||||||
uuid: uuid,
|
uuid: uuid,
|
||||||
inMemoryContent: inMemoryContent,
|
inMemoryContent: inMemoryContent,
|
||||||
fixtureDatabase: fixtureDatabase)
|
fixtureDatabase: fixtureDatabase)
|
||||||
|
|
|
@ -14,7 +14,9 @@ struct MetatextApp: App {
|
||||||
RootView(
|
RootView(
|
||||||
// swiftlint:disable force_try
|
// swiftlint:disable force_try
|
||||||
viewModel: try! RootViewModel(
|
viewModel: try! RootViewModel(
|
||||||
environment: .live(userNotificationCenter: .current()),
|
environment: .live(
|
||||||
|
userNotificationCenter: .current(),
|
||||||
|
reduceMotion: { UIAccessibility.isReduceMotionEnabled }),
|
||||||
registerForRemoteNotifications: appDelegate.registerForRemoteNotifications))
|
registerForRemoteNotifications: appDelegate.registerForRemoteNotifications))
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ class ImagePageViewController: UIPageViewController {
|
||||||
let imageViewControllers: [ImageViewController]
|
let imageViewControllers: [ImageViewController]
|
||||||
|
|
||||||
init(initiallyVisible: AttachmentViewModel, statusViewModel: StatusViewModel) {
|
init(initiallyVisible: AttachmentViewModel, statusViewModel: StatusViewModel) {
|
||||||
imageViewControllers = statusViewModel.attachmentViewModels.map(ImageViewController.init(viewModel:))
|
imageViewControllers = statusViewModel.attachmentViewModels.map { ImageViewController(viewModel: $0) }
|
||||||
|
|
||||||
super.init(
|
super.init(
|
||||||
transitionStyle: .scroll,
|
transitionStyle: .scroll,
|
||||||
|
@ -21,6 +21,17 @@ class ImagePageViewController: UIPageViewController {
|
||||||
setViewControllers([imageViewControllers[index ?? 0]], direction: .forward, animated: false)
|
setViewControllers([imageViewControllers[index ?? 0]], direction: .forward, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(imageURL: URL) {
|
||||||
|
imageViewControllers = [ImageViewController(imageURL: imageURL)]
|
||||||
|
|
||||||
|
super.init(
|
||||||
|
transitionStyle: .scroll,
|
||||||
|
navigationOrientation: .horizontal,
|
||||||
|
options: [.interPageSpacing: CGFloat.defaultSpacing])
|
||||||
|
|
||||||
|
setViewControllers(imageViewControllers, direction: .forward, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
|
|
@ -9,13 +9,15 @@ class ImageViewController: UIViewController {
|
||||||
let imageView = AnimatedImageView()
|
let imageView = AnimatedImageView()
|
||||||
let playerView = PlayerView()
|
let playerView = PlayerView()
|
||||||
|
|
||||||
private let viewModel: AttachmentViewModel
|
private let viewModel: AttachmentViewModel?
|
||||||
|
private let imageURL: URL?
|
||||||
private let contentView = UIView()
|
private let contentView = UIView()
|
||||||
private let descriptionBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
private let descriptionBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||||
private let descriptionTextView = UITextView()
|
private let descriptionTextView = UITextView()
|
||||||
|
|
||||||
init(viewModel: AttachmentViewModel) {
|
init(viewModel: AttachmentViewModel? = nil, imageURL: URL? = nil) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
self.imageURL = imageURL
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
@ -51,21 +53,22 @@ class ImageViewController: UIViewController {
|
||||||
contentView.addSubview(imageView)
|
contentView.addSubview(imageView)
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.contentMode = .scaleAspectFit
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.kf.indicatorType = .activity
|
||||||
|
|
||||||
contentView.addSubview(playerView)
|
contentView.addSubview(playerView)
|
||||||
playerView.translatesAutoresizingMaskIntoConstraints = false
|
playerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
view.addSubview(descriptionBackgroundView)
|
view.addSubview(descriptionBackgroundView)
|
||||||
descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
descriptionBackgroundView.isHidden = viewModel.attachment.description == nil
|
descriptionBackgroundView.isHidden = viewModel?.attachment.description == nil
|
||||||
|| viewModel.attachment.description == ""
|
|| viewModel?.attachment.description == ""
|
||||||
|
|
||||||
descriptionBackgroundView.contentView.addSubview(descriptionTextView)
|
descriptionBackgroundView.contentView.addSubview(descriptionTextView)
|
||||||
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
|
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
descriptionTextView.backgroundColor = .clear
|
descriptionTextView.backgroundColor = .clear
|
||||||
descriptionTextView.font = .preferredFont(forTextStyle: .caption1)
|
descriptionTextView.font = .preferredFont(forTextStyle: .caption1)
|
||||||
descriptionTextView.adjustsFontForContentSizeCategory = true
|
descriptionTextView.adjustsFontForContentSizeCategory = true
|
||||||
descriptionTextView.text = viewModel.attachment.description
|
descriptionTextView.text = viewModel?.attachment.description
|
||||||
descriptionTextView.isScrollEnabled = false
|
descriptionTextView.isScrollEnabled = false
|
||||||
descriptionTextView.isEditable = false
|
descriptionTextView.isEditable = false
|
||||||
|
|
||||||
|
@ -99,37 +102,40 @@ class ImageViewController: UIViewController {
|
||||||
descriptionTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
descriptionTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
switch viewModel.attachment.type {
|
if let viewModel = viewModel {
|
||||||
case .image:
|
switch viewModel.attachment.type {
|
||||||
imageView.tag = viewModel.tag
|
case .image:
|
||||||
|
imageView.tag = viewModel.tag
|
||||||
|
playerView.isHidden = true
|
||||||
|
imageView.kf.setImage(
|
||||||
|
with: viewModel.attachment.previewUrl,
|
||||||
|
options: [.onlyFromCache],
|
||||||
|
completionHandler: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if case .success = $0 {
|
||||||
|
self.imageView.kf.indicatorType = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
self.imageView.kf.setImage(
|
||||||
|
with: viewModel.attachment.url,
|
||||||
|
options: [.keepCurrentImageWhileLoading])
|
||||||
|
})
|
||||||
|
case .gifv:
|
||||||
|
playerView.tag = viewModel.tag
|
||||||
|
imageView.isHidden = true
|
||||||
|
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
|
||||||
|
|
||||||
|
player.isMuted = true
|
||||||
|
|
||||||
|
playerView.player = player
|
||||||
|
player.play()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
} else if let imageURL = imageURL {
|
||||||
|
imageView.tag = imageURL.hashValue
|
||||||
playerView.isHidden = true
|
playerView.isHidden = true
|
||||||
imageView.isHidden = false
|
imageView.kf.setImage(with: imageURL)
|
||||||
imageView.kf.indicatorType = .activity
|
|
||||||
imageView.kf.setImage(
|
|
||||||
with: viewModel.attachment.previewUrl,
|
|
||||||
options: [.onlyFromCache],
|
|
||||||
completionHandler: { [weak self] in
|
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
if case .success = $0 {
|
|
||||||
self.imageView.kf.indicatorType = .none
|
|
||||||
}
|
|
||||||
|
|
||||||
self.imageView.kf.setImage(
|
|
||||||
with: self.viewModel.attachment.url,
|
|
||||||
options: [.keepCurrentImageWhileLoading])
|
|
||||||
})
|
|
||||||
case .gifv:
|
|
||||||
playerView.tag = viewModel.tag
|
|
||||||
playerView.isHidden = false
|
|
||||||
imageView.isHidden = true
|
|
||||||
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
|
|
||||||
|
|
||||||
player.isMuted = true
|
|
||||||
|
|
||||||
playerView.player = player
|
|
||||||
player.play()
|
|
||||||
default: break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,19 @@ final class ProfileViewController: TableViewController {
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.imagePresentations.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let imagePageViewController = ImagePageViewController(imageURL: $0)
|
||||||
|
let imageNavigationController = ImageNavigationController(imagePageViewController: imagePageViewController)
|
||||||
|
|
||||||
|
imageNavigationController.transitionController.fromDelegate = self
|
||||||
|
self.transitionViewTag = $0.hashValue
|
||||||
|
|
||||||
|
self.present(imageNavigationController, animated: true)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
tableView.tableHeaderView = accountHeaderView
|
tableView.tableHeaderView = accountHeaderView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,14 @@ import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
class TableViewController: UITableViewController {
|
class TableViewController: UITableViewController {
|
||||||
|
var transitionViewTag = -1
|
||||||
|
|
||||||
private let viewModel: CollectionViewModel
|
private let viewModel: CollectionViewModel
|
||||||
private let identification: Identification
|
private let identification: Identification
|
||||||
private let loadingTableFooterView = LoadingTableFooterView()
|
private let loadingTableFooterView = LoadingTableFooterView()
|
||||||
private let webfingerIndicatorView = WebfingerIndicatorView()
|
private let webfingerIndicatorView = WebfingerIndicatorView()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
||||||
private var transitionViewTag = -1
|
|
||||||
|
|
||||||
private lazy var dataSource: TableViewDataSource = {
|
private lazy var dataSource: TableViewDataSource = {
|
||||||
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
|
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
|
||||||
|
@ -201,7 +202,7 @@ extension TableViewController: ZoomAnimatorDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func referenceView(for zoomAnimator: ZoomAnimator) -> UIView? {
|
func referenceView(for zoomAnimator: ZoomAnimator) -> UIView? {
|
||||||
tableView.visibleCells.compactMap { $0.viewWithTag(transitionViewTag) }.first
|
view.viewWithTag(transitionViewTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func referenceViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? {
|
func referenceViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? {
|
||||||
|
|
|
@ -7,9 +7,9 @@ import ServiceLayer
|
||||||
|
|
||||||
public struct AccountViewModel: CollectionItemViewModel {
|
public struct AccountViewModel: CollectionItemViewModel {
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
public let identification: Identification
|
|
||||||
|
|
||||||
private let accountService: AccountService
|
private let accountService: AccountService
|
||||||
|
private let identification: Identification
|
||||||
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
|
||||||
init(accountService: AccountService, identification: Identification) {
|
init(accountService: AccountService, identification: Identification) {
|
||||||
|
@ -20,13 +20,13 @@ public struct AccountViewModel: CollectionItemViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension AccountViewModel {
|
public extension AccountViewModel {
|
||||||
var avatarURL: URL { accountService.account.avatar }
|
var headerURL: URL {
|
||||||
|
if !identification.appPreferences.shouldReduceMotion, identification.appPreferences.animateHeaders {
|
||||||
var avatarStaticURL: URL { accountService.account.avatarStatic }
|
return accountService.account.header
|
||||||
|
} else {
|
||||||
var headerURL: URL { accountService.account.header }
|
return accountService.account.headerStatic
|
||||||
|
}
|
||||||
var headerStaticURL: URL { accountService.account.headerStatic }
|
}
|
||||||
|
|
||||||
var displayName: String { accountService.account.displayName }
|
var displayName: String { accountService.account.displayName }
|
||||||
|
|
||||||
|
@ -36,6 +36,16 @@ public extension AccountViewModel {
|
||||||
|
|
||||||
var emoji: [Emoji] { accountService.account.emojis }
|
var emoji: [Emoji] { accountService.account.emojis }
|
||||||
|
|
||||||
|
func avatarURL(profile: Bool = false) -> URL {
|
||||||
|
if !identification.appPreferences.shouldReduceMotion,
|
||||||
|
(identification.appPreferences.animateAvatars == .everywhere
|
||||||
|
|| identification.appPreferences.animateAvatars == .profiles && profile) {
|
||||||
|
return accountService.account.avatar
|
||||||
|
} else {
|
||||||
|
return accountService.account.avatarStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func urlSelected(_ url: URL) {
|
func urlSelected(_ url: URL) {
|
||||||
eventsSubject.send(
|
eventsSubject.send(
|
||||||
accountService.navigationService.item(url: url)
|
accountService.navigationService.item(url: url)
|
||||||
|
|
|
@ -2,15 +2,18 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
import Network
|
||||||
|
|
||||||
public struct AttachmentViewModel {
|
public struct AttachmentViewModel {
|
||||||
public let attachment: Attachment
|
public let attachment: Attachment
|
||||||
|
|
||||||
private let status: Status
|
private let status: Status
|
||||||
|
private let identification: Identification
|
||||||
|
|
||||||
init(attachment: Attachment, status: Status) {
|
init(attachment: Attachment, status: Status, identification: Identification) {
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
self.status = status
|
self.status = status
|
||||||
|
self.identification = identification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,4 +36,22 @@ public extension AttachmentViewModel {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldAutoplay: Bool {
|
||||||
|
switch attachment.type {
|
||||||
|
case .video:
|
||||||
|
return identification.appPreferences.autoplayVideos == .always
|
||||||
|
|| (identification.appPreferences.autoplayVideos == .wifi
|
||||||
|
&& Self.wifiMonitor.currentPath.status == .satisfied)
|
||||||
|
case .gifv:
|
||||||
|
return identification.appPreferences.autoplayGIFs == .always
|
||||||
|
|| (identification.appPreferences.autoplayGIFs == .wifi
|
||||||
|
&& Self.wifiMonitor.currentPath.status == .satisfied)
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AttachmentViewModel {
|
||||||
|
static let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,16 @@ final public class ProfileViewModel {
|
||||||
@Published public private(set) var accountViewModel: AccountViewModel?
|
@Published public private(set) var accountViewModel: AccountViewModel?
|
||||||
@Published public var collection = ProfileCollection.statuses
|
@Published public var collection = ProfileCollection.statuses
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
|
public let imagePresentations: AnyPublisher<URL, Never>
|
||||||
|
|
||||||
private let profileService: ProfileService
|
private let profileService: ProfileService
|
||||||
private let collectionViewModel: CurrentValueSubject<CollectionItemsViewModel, Never>
|
private let collectionViewModel: CurrentValueSubject<CollectionItemsViewModel, Never>
|
||||||
|
private let imagePresentationsSubject = PassthroughSubject<URL, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(profileService: ProfileService, identification: Identification) {
|
public init(profileService: ProfileService, identification: Identification) {
|
||||||
self.profileService = profileService
|
self.profileService = profileService
|
||||||
|
imagePresentations = imagePresentationsSubject.eraseToAnyPublisher()
|
||||||
|
|
||||||
collectionViewModel = CurrentValueSubject(
|
collectionViewModel = CurrentValueSubject(
|
||||||
CollectionItemsViewModel(
|
CollectionItemsViewModel(
|
||||||
|
@ -40,6 +43,14 @@ final public class ProfileViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension ProfileViewModel {
|
||||||
|
func presentHeader() {
|
||||||
|
guard let accountViewModel = accountViewModel else { return }
|
||||||
|
|
||||||
|
imagePresentationsSubject.send(accountViewModel.headerURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ProfileViewModel: CollectionViewModel {
|
extension ProfileViewModel: CollectionViewModel {
|
||||||
public var updates: AnyPublisher<CollectionUpdate, Never> {
|
public var updates: AnyPublisher<CollectionUpdate, Never> {
|
||||||
collectionViewModel.flatMap(\.updates).eraseToAnyPublisher()
|
collectionViewModel.flatMap(\.updates).eraseToAnyPublisher()
|
||||||
|
|
|
@ -18,10 +18,10 @@ public struct StatusViewModel: CollectionItemViewModel {
|
||||||
public let pollOptionTitles: [String]
|
public let pollOptionTitles: [String]
|
||||||
public let pollEmoji: [Emoji]
|
public let pollEmoji: [Emoji]
|
||||||
public var configuration = CollectionItem.StatusConfiguration.default
|
public var configuration = CollectionItem.StatusConfiguration.default
|
||||||
public let identification: Identification
|
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
private let statusService: StatusService
|
private let statusService: StatusService
|
||||||
|
private let identification: Identification
|
||||||
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
|
||||||
init(statusService: StatusService, identification: Identification) {
|
init(statusService: StatusService, identification: Identification) {
|
||||||
|
@ -40,7 +40,7 @@ public struct StatusViewModel: CollectionItemViewModel {
|
||||||
: statusService.status.account.displayName
|
: statusService.status.account.displayName
|
||||||
rebloggedByDisplayNameEmoji = statusService.status.account.emojis
|
rebloggedByDisplayNameEmoji = statusService.status.account.emojis
|
||||||
attachmentViewModels = statusService.status.displayStatus.mediaAttachments
|
attachmentViewModels = statusService.status.displayStatus.mediaAttachments
|
||||||
.map { AttachmentViewModel(attachment: $0, status: statusService.status) }
|
.map { AttachmentViewModel(attachment: $0, status: statusService.status, identification: identification) }
|
||||||
pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? []
|
pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? []
|
||||||
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
|
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
|
||||||
events = eventsSubject.eraseToAnyPublisher()
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
|
@ -75,9 +75,14 @@ public extension StatusViewModel {
|
||||||
|
|
||||||
var accountName: String { "@" + statusService.status.displayStatus.account.acct }
|
var accountName: String { "@" + statusService.status.displayStatus.account.acct }
|
||||||
|
|
||||||
var avatarURL: URL { statusService.status.displayStatus.account.avatar }
|
var avatarURL: URL {
|
||||||
|
if !identification.appPreferences.shouldReduceMotion,
|
||||||
var avatarStaticURL: URL { statusService.status.displayStatus.account.avatarStatic }
|
identification.appPreferences.animateAvatars == .everywhere {
|
||||||
|
return statusService.status.displayStatus.account.avatar
|
||||||
|
} else {
|
||||||
|
return statusService.status.displayStatus.account.avatarStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var time: String? { statusService.status.displayStatus.createdAt.timeAgo }
|
var time: String? { statusService.status.displayStatus.createdAt.timeAgo }
|
||||||
|
|
||||||
|
|
|
@ -5,23 +5,16 @@ import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
class AccountHeaderView: UIView {
|
class AccountHeaderView: UIView {
|
||||||
let headerImageView = UIImageView()
|
let headerImageView = AnimatedImageView()
|
||||||
|
let headerButton = UIButton()
|
||||||
let noteTextView = TouchFallthroughTextView()
|
let noteTextView = TouchFallthroughTextView()
|
||||||
let segmentedControl = UISegmentedControl()
|
let segmentedControl = UISegmentedControl()
|
||||||
|
|
||||||
var viewModel: ProfileViewModel? {
|
var viewModel: ProfileViewModel? {
|
||||||
didSet {
|
didSet {
|
||||||
if let accountViewModel = viewModel?.accountViewModel {
|
if let accountViewModel = viewModel?.accountViewModel {
|
||||||
let appPreferences = accountViewModel.identification.appPreferences
|
headerImageView.kf.setImage(with: accountViewModel.headerURL)
|
||||||
let headerURL: URL
|
headerImageView.tag = accountViewModel.headerURL.hashValue
|
||||||
|
|
||||||
if !appPreferences.shouldReduceMotion, appPreferences.animateHeaders {
|
|
||||||
headerURL = accountViewModel.headerURL
|
|
||||||
} else {
|
|
||||||
headerURL = accountViewModel.headerStaticURL
|
|
||||||
}
|
|
||||||
|
|
||||||
headerImageView.kf.setImage(with: headerURL)
|
|
||||||
|
|
||||||
let noteFont = UIFont.preferredFont(forTextStyle: .callout)
|
let noteFont = UIFont.preferredFont(forTextStyle: .callout)
|
||||||
let mutableNote = NSMutableAttributedString(attributedString: accountViewModel.note)
|
let mutableNote = NSMutableAttributedString(attributedString: accountViewModel.note)
|
||||||
|
@ -71,14 +64,25 @@ extension AccountHeaderView: UITextViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AccountHeaderView {
|
private extension AccountHeaderView {
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
let baseStackView = UIStackView()
|
let baseStackView = UIStackView()
|
||||||
|
|
||||||
addSubview(headerImageView)
|
addSubview(headerImageView)
|
||||||
addSubview(baseStackView)
|
|
||||||
headerImageView.translatesAutoresizingMaskIntoConstraints = false
|
headerImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
headerImageView.contentMode = .scaleAspectFill
|
headerImageView.contentMode = .scaleAspectFill
|
||||||
headerImageView.clipsToBounds = true
|
headerImageView.clipsToBounds = true
|
||||||
|
headerImageView.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
headerImageView.addSubview(headerButton)
|
||||||
|
headerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
headerButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
|
||||||
|
|
||||||
|
headerButton.addAction(
|
||||||
|
UIAction { [weak self] _ in self?.viewModel?.presentHeader() },
|
||||||
|
for: .touchUpInside)
|
||||||
|
|
||||||
|
addSubview(baseStackView)
|
||||||
baseStackView.translatesAutoresizingMaskIntoConstraints = false
|
baseStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
baseStackView.axis = .vertical
|
baseStackView.axis = .vertical
|
||||||
|
|
||||||
|
@ -111,6 +115,10 @@ private extension AccountHeaderView {
|
||||||
headerImageView.topAnchor.constraint(equalTo: topAnchor),
|
headerImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
headerImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
headerImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
headerImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
headerImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
headerButton.leadingAnchor.constraint(equalTo: headerImageView.leadingAnchor),
|
||||||
|
headerButton.topAnchor.constraint(equalTo: headerImageView.topAnchor),
|
||||||
|
headerButton.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor),
|
||||||
|
headerButton.trailingAnchor.constraint(equalTo: headerImageView.trailingAnchor),
|
||||||
baseStackView.topAnchor.constraint(equalTo: headerImageView.bottomAnchor),
|
baseStackView.topAnchor.constraint(equalTo: headerImageView.bottomAnchor),
|
||||||
baseStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
baseStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
|
|
@ -96,16 +96,7 @@ private extension AccountView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAccountConfiguration() {
|
func applyAccountConfiguration() {
|
||||||
let appPreferences = accountConfiguration.viewModel.identification.appPreferences
|
avatarImageView.kf.setImage(with: accountConfiguration.viewModel.avatarURL(profile: false))
|
||||||
let avatarURL: URL
|
|
||||||
|
|
||||||
if !appPreferences.shouldReduceMotion && appPreferences.animateAvatars == .everywhere {
|
|
||||||
avatarURL = accountConfiguration.viewModel.avatarURL
|
|
||||||
} else {
|
|
||||||
avatarURL = accountConfiguration.viewModel.avatarStaticURL
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarImageView.kf.setImage(with: avatarURL)
|
|
||||||
|
|
||||||
if accountConfiguration.viewModel.displayName == "" {
|
if accountConfiguration.viewModel.displayName == "" {
|
||||||
displayNameLabel.isHidden = true
|
displayNameLabel.isHidden = true
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Network
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -86,20 +85,7 @@ extension StatusAttachmentsView {
|
||||||
var shouldAutoplay: Bool {
|
var shouldAutoplay: Bool {
|
||||||
guard !isHidden, let viewModel = viewModel, viewModel.shouldShowAttachments else { return false }
|
guard !isHidden, let viewModel = viewModel, viewModel.shouldShowAttachments else { return false }
|
||||||
|
|
||||||
let appPreferences = viewModel.identification.appPreferences
|
return viewModel.attachmentViewModels.allSatisfy(\.shouldAutoplay)
|
||||||
let onWifi = NWPathMonitor(requiredInterfaceType: .wifi).currentPath.status == .satisfied
|
|
||||||
let hasVideoAttachment = viewModel.attachmentViewModels.contains { $0.attachment.type == .video }
|
|
||||||
let shouldAutoplayVideo = appPreferences.autoplayVideos == .always
|
|
||||||
|| appPreferences.autoplayVideos == .wifi && onWifi
|
|
||||||
|
|
||||||
if hasVideoAttachment && shouldAutoplayVideo {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasGIFAttachment = viewModel.attachmentViewModels.contains { $0.attachment.type == .gifv }
|
|
||||||
let shouldAutoplayGIF = appPreferences.autoplayGIFs == .always || appPreferences.autoplayGIFs == .wifi && onWifi
|
|
||||||
|
|
||||||
return hasGIFAttachment && shouldAutoplayGIF
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -292,22 +292,14 @@ private extension StatusView {
|
||||||
|
|
||||||
func applyStatusConfiguration() {
|
func applyStatusConfiguration() {
|
||||||
let viewModel = statusConfiguration.viewModel
|
let viewModel = statusConfiguration.viewModel
|
||||||
let appPreferences = viewModel.identification.appPreferences
|
|
||||||
let isContextParent = viewModel.configuration.isContextParent
|
let isContextParent = viewModel.configuration.isContextParent
|
||||||
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
||||||
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
||||||
let contentFont = UIFont.preferredFont(forTextStyle: isContextParent ? .title3 : .callout)
|
let contentFont = UIFont.preferredFont(forTextStyle: isContextParent ? .title3 : .callout)
|
||||||
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
||||||
let avatarURL: URL
|
|
||||||
|
|
||||||
if !appPreferences.shouldReduceMotion && appPreferences.animateAvatars == .everywhere {
|
avatarImageView.kf.setImage(with: viewModel.avatarURL)
|
||||||
avatarURL = viewModel.avatarURL
|
|
||||||
} else {
|
|
||||||
avatarURL = viewModel.avatarStaticURL
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarImageView.kf.setImage(with: avatarURL)
|
|
||||||
|
|
||||||
contentTextView.shouldFallthrough = !isContextParent
|
contentTextView.shouldFallthrough = !isContextParent
|
||||||
sideStackView.isHidden = isContextParent
|
sideStackView.isHidden = isContextParent
|
||||||
|
|
Loading…
Reference in a new issue